| 1 | import { chat_metadata, characters, substituteParams, chat, extension_prompt_roles, extension_prompt_types, name2, neutralCharacterName } from '../../script.js'; |
| 2 | import { extension_settings } from '../extensions.js'; |
| 3 | import { getGroupMembers, groups } from '../group-chats.js'; |
| 4 | import { power_user } from '../power-user.js'; |
| 5 | import { searchCharByName, getTagsList, tags, tag_map } from '../tags.js'; |
| 6 | import { onlyUniqueJson, sortIgnoreCaseAndAccents } from '../utils.js'; |
| 7 | import { world_names } from '../world-info.js'; |
| 8 | import { SlashCommandClosure } from './SlashCommandClosure.js'; |
| 9 | import { SlashCommandEnumValue, enumTypes } from './SlashCommandEnumValue.js'; |
| 10 | |
| 11 | /** @typedef {import('./SlashCommandExecutor.js').SlashCommandExecutor} SlashCommandExecutor */ |
| 12 | /** @typedef {import('./SlashCommandScope.js').SlashCommandScope} SlashCommandScope */ |
| 13 | |
| 14 | /** |
| 15 | * A collection of regularly used enum icons |
| 16 | */ |
| 17 | export const enumIcons = { |
| 18 | default: '◊', |
| 19 | |
| 20 | // Variables |
| 21 | variable: '𝑥', |
| 22 | localVariable: 'L', |
| 23 | globalVariable: 'G', |
| 24 | scopeVariable: 'S', |
| 25 | |
| 26 | // Common types |
| 27 | character: '👤', |
| 28 | group: '🧑🤝🧑', |
| 29 | persona: '🧙♂️', |
| 30 | qr: 'QR', |
| 31 | closure: '𝑓', |
| 32 | macro: '{{', |
| 33 | tag: '🏷️', |
| 34 | world: '🌐', |
| 35 | preset: '⚙️', |
| 36 | file: '📄', |
| 37 | message: '💬', |
| 38 | reasoning: '💡', |
| 39 | voice: '🎤', |
| 40 | server: '🖥️', |
| 41 | popup: '🗔', |
| 42 | image: '🖼️', |
| 43 | video: '🎥', |
| 44 | key: '🔑', |
| 45 | spinner: '♻️', |
| 46 | stop: '🛑', |
| 47 | |
| 48 | true: '✔️', |
| 49 | false: '❌', |
| 50 | null: '🚫', |
| 51 | undefined: '❓', |
| 52 | |
| 53 | // Value types |
| 54 | boolean: '🔲', |
| 55 | string: '📝', |
| 56 | number: '1️⃣', |
| 57 | array: '[]', |
| 58 | enum: '📚', |
| 59 | dictionary: '{}', |
| 60 | |
| 61 | // Roles |
| 62 | system: '⚙️', |
| 63 | user: '👤', |
| 64 | assistant: '🤖', |
| 65 | |
| 66 | // WI Icons |
| 67 | constant: '🔵', |
| 68 | normal: '🟢', |
| 69 | disabled: '❌', |
| 70 | vectorized: '🔗', |
| 71 | |
| 72 | /** |
| 73 | * Returns the appropriate state icon based on a boolean |
| 74 | * |
| 75 | * @param {boolean} state - The state to determine the icon for |
| 76 | * @returns {string} The corresponding state icon |
| 77 | */ |
| 78 | getStateIcon: (state) => { |
| 79 | return state ? enumIcons.true : enumIcons.false; |
| 80 | }, |
| 81 | |
| 82 | /** |
| 83 | * Returns the appropriate WI icon based on the entry |
| 84 | * |
| 85 | * @param {Object} entry - WI entry |
| 86 | * @returns {string} The corresponding WI icon |
| 87 | */ |
| 88 | getWiStatusIcon: (entry) => { |
| 89 | if (entry.constant) return enumIcons.constant; |
| 90 | if (entry.disable) return enumIcons.disabled; |
| 91 | if (entry.vectorized) return enumIcons.vectorized; |
| 92 | return enumIcons.normal; |
| 93 | }, |
| 94 | |
| 95 | /** |
| 96 | * Returns the appropriate icon based on the role |
| 97 | * |
| 98 | * @param {extension_prompt_roles} role - The role to get the icon for |
| 99 | * @returns {string} The corresponding icon |
| 100 | */ |
| 101 | getRoleIcon: (role) => { |
| 102 | switch (role) { |
| 103 | case extension_prompt_roles.SYSTEM: return enumIcons.system; |
| 104 | case extension_prompt_roles.USER: return enumIcons.user; |
| 105 | case extension_prompt_roles.ASSISTANT: return enumIcons.assistant; |
| 106 | default: return enumIcons.default; |
| 107 | } |
| 108 | }, |
| 109 | |
| 110 | /** |
| 111 | * A function to get the data type icon |
| 112 | * |
| 113 | * @param {string} type - The type of the data |
| 114 | * @returns {string} The corresponding data type icon |
| 115 | */ |
| 116 | getDataTypeIcon: (type) => { |
| 117 | // Remove possible nullable types definition to match type icon |
| 118 | type = type.replace(/\?$/, ''); |
| 119 | return enumIcons[type] ?? enumIcons.default; |
| 120 | }, |
| 121 | }; |
| 122 | |
| 123 | /** |
| 124 | * A collection of common enum providers |
| 125 | * |
| 126 | * Can be used on `SlashCommandNamedArgument` and `SlashCommandArgument` and their `enumProvider` property. |
| 127 | */ |
| 128 | export const commonEnumProviders = { |
| 129 | /** |
| 130 | * Enum values for booleans. Either using true/false or on/off |
| 131 | * Optionally supports "toggle". |
| 132 | * |
| 133 | * @param {('onOff'|'onOffToggle'|'trueFalse')?} [mode='trueFalse'] - The mode to use. Default is 'trueFalse'. |
| 134 | * @returns {() => SlashCommandEnumValue[]} |
| 135 | */ |
| 136 | boolean: (mode = 'trueFalse') => () => { |
| 137 | switch (mode) { |
| 138 | case 'onOff': return [new SlashCommandEnumValue('on', null, 'macro', enumIcons.true), new SlashCommandEnumValue('off', null, 'macro', enumIcons.false)]; |
| 139 | case 'onOffToggle': return [new SlashCommandEnumValue('on', null, 'macro', enumIcons.true), new SlashCommandEnumValue('off', null, 'macro', enumIcons.false), new SlashCommandEnumValue('toggle', null, 'macro', enumIcons.boolean)]; |
| 140 | case 'trueFalse': return [new SlashCommandEnumValue('true', null, 'macro', enumIcons.true), new SlashCommandEnumValue('false', null, 'macro', enumIcons.false)]; |
| 141 | default: throw new Error(`Invalid boolean enum provider mode: ${mode}`); |
| 142 | } |
| 143 | }, |
| 144 | |
| 145 | /** |
| 146 | * All possible variable names |
| 147 | * |
| 148 | * Can be filtered by `type` to only show global or local variables |
| 149 | * |
| 150 | * @param {...('global'|'local'|'scope'|'all')} type - The type of variables to include in the array. Can be 'all', 'global', or 'local'. |
| 151 | * @returns {(executor:SlashCommandExecutor, scope:SlashCommandScope) => SlashCommandEnumValue[]} |
| 152 | */ |
| 153 | variables: (...type) => (_, scope) => { |
| 154 | const types = type.flat(); |
| 155 | const isAll = types.includes('all'); |
| 156 | return [ |
| 157 | ...isAll || types.includes('scope') ? scope.allVariableNames.map(name => new SlashCommandEnumValue(name, null, enumTypes.variable, enumIcons.scopeVariable)) : [], |
| 158 | ...isAll || types.includes('local') ? Object.keys(chat_metadata.variables ?? []).map(name => new SlashCommandEnumValue(name, null, enumTypes.name, enumIcons.localVariable)) : [], |
| 159 | ...isAll || types.includes('global') ? Object.keys(extension_settings.variables.global ?? []).map(name => new SlashCommandEnumValue(name, null, enumTypes.macro, enumIcons.globalVariable)) : [], |
| 160 | ].filter((item, idx, list) => idx == list.findIndex(it => it.value == item.value)); |
| 161 | }, |
| 162 | |
| 163 | /** |
| 164 | * Enum values for numbers and variable names |
| 165 | * |
| 166 | * Includes all variable names and the ability to specify any number |
| 167 | * |
| 168 | * @param {SlashCommandExecutor} executor - The executor of the slash command |
| 169 | * @param {SlashCommandScope} scope - The scope of the slash command |
| 170 | * @returns {SlashCommandEnumValue[]} The enum values |
| 171 | */ |
| 172 | numbersAndVariables: (executor, scope) => [ |
| 173 | ...commonEnumProviders.variables('all')(executor, scope), |
| 174 | new SlashCommandEnumValue( |
| 175 | 'any variable name', |
| 176 | null, |
| 177 | enumTypes.variable, |
| 178 | enumIcons.variable, |
| 179 | (input) => /^\w*$/.test(input), |
| 180 | (input) => input, |
| 181 | ), |
| 182 | new SlashCommandEnumValue( |
| 183 | 'any number', |
| 184 | null, |
| 185 | enumTypes.number, |
| 186 | enumIcons.number, |
| 187 | (input) => input == '' || !Number.isNaN(Number(input)), |
| 188 | (input) => input, |
| 189 | ), |
| 190 | ], |
| 191 | |
| 192 | /** |
| 193 | * All possible char entities, like characters and groups. Can be filtered down to just one type. |
| 194 | * |
| 195 | * @param {('all' | 'character' | 'group')?} [mode='all'] - Which type to return |
| 196 | * @returns {() => SlashCommandEnumValue[]} |
| 197 | */ |
| 198 | characters: (mode = 'all') => () => { |
| 199 | return [ |
| 200 | ...['all', 'character'].includes(mode) ? characters.map(char => new SlashCommandEnumValue(char.name, null, enumTypes.name, enumIcons.character)) : [], |
| 201 | ...['all', 'group'].includes(mode) ? groups.map(group => new SlashCommandEnumValue(group.name, null, enumTypes.qr, enumIcons.group)) : [], |
| 202 | ...(name2 === neutralCharacterName) ? [new SlashCommandEnumValue(neutralCharacterName, null, enumTypes.name, '🥸')] : [], |
| 203 | ]; |
| 204 | }, |
| 205 | |
| 206 | /** |
| 207 | * All group members of the given group, or default the current active one |
| 208 | * |
| 209 | * @param {string?} groupId - The id of the group - pass in `undefined` to use the current active group |
| 210 | * @returns {() =>SlashCommandEnumValue[]} |
| 211 | */ |
| 212 | groupMembers: (groupId = undefined) => () => getGroupMembers(groupId).map((character, index) => new SlashCommandEnumValue(String(index), character.name, enumTypes.enum, enumIcons.character)), |
| 213 | |
| 214 | /** |
| 215 | * All possible personas |
| 216 | * |
| 217 | * @returns {() => SlashCommandEnumValue[]} |
| 218 | */ |
| 219 | personas: ({ allowPersonaKey = false } = {}) => () => Object.entries(power_user.personas).map(([personaKey, personaName]) => { |
| 220 | const existsMultiple = Object.values(power_user.personas).filter(p => p === personaName).length > 1; |
| 221 | const returnValue = allowPersonaKey && existsMultiple ? personaKey : personaName; |
| 222 | return new SlashCommandEnumValue(returnValue, allowPersonaKey && existsMultiple ? personaName : null, enumTypes.name, enumIcons.persona); |
| 223 | }), |
| 224 | |
| 225 | /** |
| 226 | * All possible tags, or only those that have been assigned |
| 227 | * |
| 228 | * @param {('all' | 'assigned')} [mode='all'] - Which types of tags to show |
| 229 | * @returns {() => SlashCommandEnumValue[]} |
| 230 | */ |
| 231 | tags: (mode = 'all') => () => { |
| 232 | let assignedTags = mode === 'assigned' ? new Set(Object.values(tag_map).flat()) : new Set(); |
| 233 | return tags.filter(tag => mode === 'all' || (mode === 'assigned' && assignedTags.has(tag.id))) |
| 234 | .map(tag => new SlashCommandEnumValue(tag.name, null, enumTypes.command, enumIcons.tag)); |
| 235 | }, |
| 236 | |
| 237 | /** |
| 238 | * All possible tags for a given char/group entity |
| 239 | * |
| 240 | * @param {('all' | 'existing' | 'not-existing')?} [mode='all'] - Which types of tags to show |
| 241 | * @returns {(executor:SlashCommandExecutor, scope:SlashCommandScope) => SlashCommandEnumValue[]} |
| 242 | */ |
| 243 | tagsForChar: (mode = 'all') => (executor, _scope) => { |
| 244 | // Try to see if we can find the char during execution to filter down the tags list some more. Otherwise take all tags. |
| 245 | const charName = executor.namedArgumentList.find(it => it.name == 'name')?.value; |
| 246 | if (charName instanceof SlashCommandClosure) throw new Error('Argument \'name\' does not support closures'); |
| 247 | const key = searchCharByName(substituteParams(charName), { suppressLogging: true }); |
| 248 | const assigned = key ? getTagsList(key) : []; |
| 249 | return tags.filter(it => mode === 'all' || mode === 'existing' && assigned.includes(it) || mode === 'not-existing' && !assigned.includes(it)) |
| 250 | .map(tag => new SlashCommandEnumValue(tag.name, null, enumTypes.command, enumIcons.tag)); |
| 251 | }, |
| 252 | |
| 253 | /** |
| 254 | * All messages in the current chat, returning the message id |
| 255 | * |
| 256 | * Optionally supports variable names, and/or a placeholder for the last/new message id |
| 257 | * |
| 258 | * @param {object} [options={}] - Optional arguments |
| 259 | * @param {boolean} [options.allowIdAfter=false] - Whether to add an enum option for the new message id after the last message |
| 260 | * @param {boolean} [options.allowVars=false] - Whether to add enum option for variable names |
| 261 | * @returns {(executor:SlashCommandExecutor, scope:SlashCommandScope) => SlashCommandEnumValue[]} |
| 262 | */ |
| 263 | messages: ({ allowIdAfter = false, allowVars = false } = {}) => (executor, scope) => { |
| 264 | const nameFilter = executor.namedArgumentList.find(it => it.name == 'name')?.value || ''; |
| 265 | return [ |
| 266 | ...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)).filter(value => !nameFilter || value.description.startsWith(`${nameFilter}:`)), |
| 267 | ...allowIdAfter ? [new SlashCommandEnumValue(String(chat.length), '>> After Last Message >>', enumTypes.enum, '➕')] : [], |
| 268 | ...allowVars ? commonEnumProviders.variables('all')(executor, scope) : [], |
| 269 | ]; |
| 270 | }, |
| 271 | |
| 272 | /** |
| 273 | * Media items attached to a specific message |
| 274 | * @returns {(executor:SlashCommandExecutor, scope:SlashCommandScope) => SlashCommandEnumValue[]} |
| 275 | */ |
| 276 | messageMedia: () => (executor, _scope) => { |
| 277 | const messageId = Number(executor.namedArgumentList.find(it => ['mesId', 'id'].includes(it.name))?.value || ''); |
| 278 | if (isNaN(messageId) || messageId === null || messageId < 0 || messageId >= chat.length) { |
| 279 | return []; |
| 280 | } |
| 281 | const message = chat[messageId]; |
| 282 | if (!Array.isArray(message?.extra?.media)) { |
| 283 | return []; |
| 284 | } |
| 285 | return message.extra.media.map((media, index) => new SlashCommandEnumValue(index.toString(), media.title || message.extra.title || '[Untitled]', enumTypes.enum, enumIcons[media.type] || enumIcons.file)); |
| 286 | }, |
| 287 | |
| 288 | /** |
| 289 | * All names used in the current chat. |
| 290 | * |
| 291 | * @returns {SlashCommandEnumValue[]} |
| 292 | */ |
| 293 | messageNames: () => chat |
| 294 | .map(message => ({ |
| 295 | name: message.name, |
| 296 | icon: message.is_user ? enumIcons.user : enumIcons.assistant, |
| 297 | })) |
| 298 | .filter(onlyUniqueJson) |
| 299 | .sort((a, b) => sortIgnoreCaseAndAccents(a.name, b.name)) |
| 300 | .map(name => new SlashCommandEnumValue(name.name, null, null, name.icon)), |
| 301 | |
| 302 | /** |
| 303 | * All existing worlds / lorebooks |
| 304 | * |
| 305 | * @returns {SlashCommandEnumValue[]} |
| 306 | */ |
| 307 | worlds: () => world_names.map(worldName => new SlashCommandEnumValue(worldName, null, enumTypes.name, enumIcons.world)), |
| 308 | |
| 309 | /** |
| 310 | * All existing injects for the current chat |
| 311 | * |
| 312 | * @returns {SlashCommandEnumValue[]} |
| 313 | */ |
| 314 | injects: () => { |
| 315 | if (!chat_metadata.script_injects || !Object.keys(chat_metadata.script_injects).length) return []; |
| 316 | return Object.entries(chat_metadata.script_injects) |
| 317 | .map(([id, inject]) => { |
| 318 | const positionName = (Object.entries(extension_prompt_types)).find(([_, value]) => value === inject.position)?.[0] ?? 'unknown'; |
| 319 | return new SlashCommandEnumValue(id, `${enumIcons.getRoleIcon(inject.role ?? extension_prompt_roles.SYSTEM)}[Inject](${positionName}, depth: ${inject.depth}, scan: ${inject.scan ?? false}) ${inject.value}`, |
| 320 | enumTypes.enum, '💉'); |
| 321 | }); |
| 322 | }, |
| 323 | |
| 324 | /** |
| 325 | * Gets somewhat recognizable STscript types. |
| 326 | * |
| 327 | * @returns {SlashCommandEnumValue[]} |
| 328 | */ |
| 329 | types: () => [ |
| 330 | new SlashCommandEnumValue('string', null, enumTypes.type, enumIcons.string), |
| 331 | new SlashCommandEnumValue('number', null, enumTypes.type, enumIcons.number), |
| 332 | new SlashCommandEnumValue('boolean', null, enumTypes.type, enumIcons.boolean), |
| 333 | new SlashCommandEnumValue('array', null, enumTypes.type, enumIcons.array), |
| 334 | new SlashCommandEnumValue('object', null, enumTypes.type, enumIcons.dictionary), |
| 335 | new SlashCommandEnumValue('null', null, enumTypes.type, enumIcons.null), |
| 336 | new SlashCommandEnumValue('undefined', null, enumTypes.type, enumIcons.undefined), |
| 337 | ], |
| 338 | |
| 339 | messageRoles: () => [ |
| 340 | new SlashCommandEnumValue('user', null, enumTypes.enum, enumIcons.user), |
| 341 | new SlashCommandEnumValue('assistant', null, enumTypes.enum, enumIcons.assistant), |
| 342 | new SlashCommandEnumValue('system', null, enumTypes.enum, enumIcons.system), |
| 343 | ], |
| 344 | |
| 345 | backgrounds: () => Array.from(document.querySelectorAll('.bg_example')) |
| 346 | .map(it => new SlashCommandEnumValue(it.getAttribute('bgfile'))) |
| 347 | .filter(it => it.value?.length), |
| 348 | |
| 349 | connectionProfiles: ({ includeNone = false } = {}) => () => [ |
| 350 | ...includeNone ? [new SlashCommandEnumValue('<None>')] : [], |
| 351 | ...extension_settings.connectionManager.profiles.map(p => new SlashCommandEnumValue(p.name, null, enumTypes.name, enumIcons.server)), |
| 352 | ], |
| 353 | }; |
| 354 | |
| 355 | |
| 356 | /** |
| 357 | * A collection of common enum match providers |
| 358 | * |
| 359 | * Can be used on `SlashCommandEnumValue` and their `matchProvider` property. |
| 360 | */ |
| 361 | export const commonEnumMatchProviders = { |
| 362 | /** |
| 363 | * Provides autocomplete matching for folder-like enum values. |
| 364 | * Matches if the input starts with the check or vice versa (case-insensitive). |
| 365 | * @param {string} input - The input string to match against |
| 366 | * @param {string} check - The check string to match with |
| 367 | * @param {object} [options={}] - Options |
| 368 | * @param {boolean} [options.trueOnEmpty=true] - Whether to return true when input is empty |
| 369 | * @returns {boolean} - True if the strings match according to the folder matching rules |
| 370 | */ |
| 371 | folderEnum: (input, check, { trueOnEmpty = true } = {}) => { |
| 372 | if (!check) return false; |
| 373 | if (!input) return trueOnEmpty; |
| 374 | const inputLower = input.toLowerCase(); |
| 375 | const checkLower = check.toLowerCase(); |
| 376 | return inputLower.startsWith(checkLower) || checkLower.startsWith(inputLower); |
| 377 | }, |
| 378 | }; |