add fuzzy and include matching for autocomplete

This commit is contained in:
LenAnderson
2024-03-25 10:44:52 -04:00
parent 488fe15ab6
commit da6372cf86
6 changed files with 106 additions and 6 deletions

View File

@ -3567,6 +3567,14 @@
</label> </label>
</div> </div>
<h4><span data-i18n="Miscellaneous">Miscellaneous</span></h4> <h4><span data-i18n="Miscellaneous">Miscellaneous</span></h4>
<div title="Determines how STscript commands are found for autocomplete." data-i18n="[title]Determines how STscript commands are found for autocomplete.">
<label for="stscript_matching" data-i18n="STscript Matching">STscript Matching</label>
<select id="stscript_matching">
<option data-i18n="Starts with" value="strict">Starts with</option>
<option data-i18n="Includes" value="includes">Includes</option>
<option data-i18n="Fuzzy" value="fuzzy">Fuzzy</option>
</select>
</div>
<div title="If set in the advanced character definitions, this field will be displayed in the characters list." data-i18n="[title]If set in the advanced character definitions, this field will be displayed in the characters list."> <div title="If set in the advanced character definitions, this field will be displayed in the characters list." data-i18n="[title]If set in the advanced character definitions, this field will be displayed in the characters list.">
<label for="aux_field" data-i18n="Aux List Field">Aux List Field</label> <label for="aux_field" data-i18n="Aux List Field">Aux List Field</label>
<select id="aux_field"> <select id="aux_field">

View File

@ -237,6 +237,9 @@ let power_user = {
bogus_folders: false, bogus_folders: false,
show_tag_filters: false, show_tag_filters: false,
aux_field: 'character_version', aux_field: 'character_version',
stscript: {
matching: 'fuzzy',
},
restore_user_input: true, restore_user_input: true,
reduced_motion: false, reduced_motion: false,
compact_input_area: true, compact_input_area: true,
@ -1527,6 +1530,7 @@ function loadPowerUserSettings(settings, data) {
$('#chat_width_slider').val(power_user.chat_width); $('#chat_width_slider').val(power_user.chat_width);
$('#token_padding').val(power_user.token_padding); $('#token_padding').val(power_user.token_padding);
$('#aux_field').val(power_user.aux_field); $('#aux_field').val(power_user.aux_field);
$('#stscript_matching').val(power_user.stscript.matching);
$('#restore_user_input').prop('checked', power_user.restore_user_input); $('#restore_user_input').prop('checked', power_user.restore_user_input);
$('#chat_truncation').val(power_user.chat_truncation); $('#chat_truncation').val(power_user.chat_truncation);
@ -3372,6 +3376,12 @@ $(document).ready(() => {
printCharacters(false); printCharacters(false);
}); });
$('#stscript_matching').on('change', function () {
const value = $(this).find(':selected').val();
power_user.stscript.matching = String(value);
saveSettingsDebounced();
});
$('#restore_user_input').on('input', function () { $('#restore_user_input').on('input', function () {
power_user.restore_user_input = !!$(this).prop('checked'); power_user.restore_user_input = !!$(this).prop('checked');
saveSettingsDebounced(); saveSettingsDebounced();

View File

@ -47,7 +47,7 @@ import { autoSelectPersona } from './personas.js';
import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js'; import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js'; import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
import { decodeTextTokens, getFriendlyTokenizerName, getTextTokens, getTokenCount } from './tokenizers.js'; import { decodeTextTokens, getFriendlyTokenizerName, getTextTokens, getTokenCount } from './tokenizers.js';
import { debounce, delay, isFalseBoolean, isTrueBoolean, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js'; import { debounce, delay, escapeRegex, isFalseBoolean, isTrueBoolean, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
import { registerVariableCommands, resolveVariable } from './variables.js'; import { registerVariableCommands, resolveVariable } from './variables.js';
export { export {
executeSlashCommands, getSlashCommandsHelp, registerSlashCommand, executeSlashCommands, getSlashCommandsHelp, registerSlashCommand,
@ -1890,7 +1890,7 @@ function setSlashCommandAutocomplete(textarea) {
element.selectionStart = executor.start + u.item.value.length - 2; element.selectionStart = executor.start + u.item.value.length - 2;
element.selectionEnd = element.selectionStart; element.selectionEnd = element.selectionStart;
} else { } else {
console.log('[AUTOCOMPLETE]', '[SELECT]', {e, u}); console.log('[AUTOCOMPLETE]', '[SELECT]', { e, u });
} }
}, },
focus: (e, u) => { focus: (e, u) => {
@ -1975,17 +1975,92 @@ export function setNewSlashCommandAutoComplete(textarea, isFloating = false) {
const slashCommand = executor?.name?.toLowerCase() ?? ''; const slashCommand = executor?.name?.toLowerCase() ?? '';
isReplacable = isInput && (!executor ? true : textarea.selectionStart == executor.start - 2 + executor.name.length + 1); isReplacable = isInput && (!executor ? true : textarea.selectionStart == executor.start - 2 + executor.name.length + 1);
const matchType = power_user.stscript?.matching ?? 'strict';
const fuzzyRegex = new RegExp(`^(.*)${slashCommand.split('').map(char=>`(${escapeRegex(char)})`).join('(.*)')}(.*)$`, 'i');
const matchers = {
'strict': (cmd) => cmd.toLowerCase().startsWith(slashCommand),
'includes': (cmd) => cmd.toLowerCase().includes(slashCommand),
'fuzzy': (cmd) => fuzzyRegex.test(cmd),
};
const fuzzyScore = (name) => {
const parts = fuzzyRegex.exec(name).slice(1, -1);
let start = null;
let consecutive = [];
let current = '';
let offset = 0;
parts.forEach((part, idx) => {
if (idx % 2 == 0) {
if (part.length > 0) {
if (current.length > 0) {
consecutive.push(current);
}
current = '';
}
} else {
if (start === null) {
start = offset;
}
current += part;
}
offset += part.length;
});
if (current.length > 0) {
consecutive.push(current);
}
consecutive.sort((a,b)=>b.length - a.length);
console.log({ name, parts, start, consecutive, longestConsecutive:consecutive[0]?.length ?? 0 });
return { name, start, longestConsecutive:consecutive[0]?.length ?? 0 };
};
const fuzzyScoreCompare = (a, b) => {
if (a.score.start < b.score.start) return -1;
if (a.score.start > b.score.start) return 1;
if (a.score.longestConsecutive > b.score.longestConsecutive) return -1;
if (a.score.longestConsecutive < b.score.longestConsecutive) return 1;
return a.name.localeCompare(b.name);
};
const buildHelpStringName = (name) => {
switch (matchType) {
case 'strict': {
return `<span class="monospace">/<span class="matched">${name.slice(0, slashCommand.length)}</span>${name.slice(slashCommand.length)}</span> `;
}
case 'includes': {
const start = name.toLowerCase().search(slashCommand);
return `<span class="monospace">/${name.slice(0, start)}<span class="matched">${name.slice(start, start + slashCommand.length)}</span>${name.slice(start + slashCommand.length)}</span> `;
}
case 'fuzzy': {
const matched = name.replace(fuzzyRegex, (_, ...parts)=>{
parts.splice(-2, 2);
return parts.map((it, idx)=>{
if (it === null || it.length == 0) return '';
if (idx % 2 == 1) {
return `<span class="matched">${it}</span>`;
}
return it;
}).join('');
});
return `<span class="monospace">/${matched}</span> `;
}
}
};
// don't show if no executor found, i.e. cursor's area is not a command // don't show if no executor found, i.e. cursor's area is not a command
if (!executor) return hide(); if (!executor) return hide();
else { else {
const helpStrings = Object const helpStrings = Object
.keys(parser.commands) // Get all slash commands .keys(parser.commands) // Get all slash commands
.filter(it => executor.name == '' || isReplacable ? it.toLowerCase().startsWith(slashCommand) : it.toLowerCase() == slashCommand) // Filter by the input .filter(it => executor.name == '' || isReplacable ? matchers[matchType](it) : it.toLowerCase() == slashCommand) // Filter by the input
.sort((a, b) => a.localeCompare(b)) // Sort alphabetically // .sort((a, b) => a.localeCompare(b)) // Sort alphabetically
; ;
result = helpStrings result = helpStrings
.filter((it,idx)=>[idx, -1].includes(helpStrings.indexOf(parser.commands[it].name.toLowerCase()))) // remove duplicates .filter((it,idx)=>[idx, -1].includes(helpStrings.indexOf(parser.commands[it].name.toLowerCase()))) // remove duplicates
.map(it => ({ label: parser.commands[it].helpStringFormatted, value: `/${it}`, li:null })) // Map to the help string .map(it => ({
name: it,
label: `${buildHelpStringName(it)}${parser.commands[it].helpStringFormattedWithoutName}`,
value: `/${it}`,
score: matchType == 'fuzzy' ? fuzzyScore(it) : null,
li: null,
})) // Map to the help string
.toSorted(matchType == 'fuzzy' ? fuzzyScoreCompare : (a, b) => a.name.localeCompare(b.name))
; ;
} }

View File

@ -3,6 +3,7 @@ export class SlashCommand {
/**@type {Function}*/ callback; /**@type {Function}*/ callback;
/**@type {String}*/ helpString; /**@type {String}*/ helpString;
/**@type {String}*/ helpStringFormatted; /**@type {String}*/ helpStringFormatted;
/**@type {String}*/ helpStringFormattedWithoutName;
/**@type {Boolean}*/ interruptsGeneration; /**@type {Boolean}*/ interruptsGeneration;
/**@type {Boolean}*/ purgeFromMessage; /**@type {Boolean}*/ purgeFromMessage;
/**@type {String[]}*/ aliases; /**@type {String[]}*/ aliases;

View File

@ -48,11 +48,13 @@ export class SlashCommandParser {
}); });
} }
let stringBuilder = `<span class="monospace">/${command}</span> ${helpString} `; let stringBuilder = `${helpString} `;
if (Array.isArray(aliases) && aliases.length) { if (Array.isArray(aliases) && aliases.length) {
let aliasesString = `(alias: ${aliases.map(x => `<span class="monospace">/${x}</span>`).join(', ')})`; let aliasesString = `(alias: ${aliases.map(x => `<span class="monospace">/${x}</span>`).join(', ')})`;
stringBuilder += aliasesString; stringBuilder += aliasesString;
} }
fnObj.helpStringFormattedWithoutName = stringBuilder;
stringBuilder = `<span class="monospace">/${command}</span> ${stringBuilder}`;
this.helpStrings[command] = stringBuilder; this.helpStrings[command] = stringBuilder;
fnObj.helpStringFormatted = stringBuilder; fnObj.helpStringFormatted = stringBuilder;
} }

View File

@ -1062,6 +1062,10 @@ select {
background-color: #20395C; background-color: #20395C;
color: white; color: white;
} }
.matched {
color: #6CABFB;
font-weight: bold;
}
} }
} }