Blame Raw
Cohee · 51ad27fb · · 378 lines (16.2 KB)
2 contributors
1import { chat_metadata, characters, substituteParams, chat, extension_prompt_roles, extension_prompt_types, name2, neutralCharacterName } from '../../script.js';
2import { extension_settings } from '../extensions.js';
3import { getGroupMembers, groups } from '../group-chats.js';
4import { power_user } from '../power-user.js';
5import { searchCharByName, getTagsList, tags, tag_map } from '../tags.js';
6import { onlyUniqueJson, sortIgnoreCaseAndAccents } from '../utils.js';
7import { world_names } from '../world-info.js';
8import { SlashCommandClosure } from './SlashCommandClosure.js';
9import { 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 */
17export 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 */
128export 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 */
361export 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};