mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
add fuzzy and include matching for autocomplete
This commit is contained in:
@ -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">
|
||||||
|
@ -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();
|
||||||
|
@ -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))
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -1062,6 +1062,10 @@ select {
|
|||||||
background-color: #20395C;
|
background-color: #20395C;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
.matched {
|
||||||
|
color: #6CABFB;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user