diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index 451d968d8..b06ef3719 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -35,7 +35,7 @@ import { } from './instruct-mode.js'; import { registerSlashCommand } from './slash-commands.js'; -import { tags } from './tags.js'; +import { tag_map, tags } from './tags.js'; import { tokenizers } from './tokenizers.js'; import { BIAS_CACHE } from './logit-bias.js'; import { renderTemplateAsync } from './templates.js'; @@ -2327,9 +2327,65 @@ function doNewChat() { }, 1); } -async function doRandomChat() { +/** + * 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 = Math.floor(Math.random() * characters.length).toString(); + const characterId = getRandomCharacterId(); + if (!characterId) { + toastr.error('No characters found'); + return; + } setCharacterId(characterId); setActiveCharacter(characters[characterId]?.avatar); setActiveGroup(null); @@ -3522,7 +3578,7 @@ $(document).ready(() => { registerSlashCommand('vn', toggleWaifu, [], '– swaps Visual Novel Mode On/Off', false, true); registerSlashCommand('newchat', doNewChat, [], '– start a new chat with current character', true, true); - registerSlashCommand('random', doRandomChat, [], '– start a new chat with a random character', true, true); + registerSlashCommand('random', doRandomChat, [], '(optional tag name) – start a new chat with a random character. If an argument is provided, only considers characters that have the specified tag.', true, true); registerSlashCommand('delmode', doDelMode, ['del'], '(optional number) – enter message deletion mode, and auto-deletes last N messages if numeric argument is provided', true, true); registerSlashCommand('cut', doMesCut, [], '(number or range) – cuts the specified message or continuous chunk from the chat, e.g. /cut 0-10. Ranges are inclusive! Returns the text of cut messages separated by a newline.', true, true); registerSlashCommand('resetpanels', doResetPanels, ['resetui'], '– resets UI panels to original state.', true, true);