mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Merge pull request #2165 from Wolfsblvt/scored-search-sorting
Scored search sorting
This commit is contained in:
@@ -3410,6 +3410,7 @@
|
||||
<div id="world_popup_delete" class="menu_button fa-solid fa-trash-can redWarningBG" title="Delete World Info" data-i18n="[title]Delete World Info"></div>
|
||||
<input type="search" class="text_pole textarea_compact" data-i18n="[placeholder]Search..." id="world_info_search" placeholder="Search...">
|
||||
<select id="world_info_sort_order" class="margin0">
|
||||
<option data-rule="search" value="14" data-i18n="Search" hidden>Search</option>
|
||||
<option data-rule="priority" value="0" data-i18n="Priority">Priority</option>
|
||||
<option data-rule="custom" value="13" data-i18n="Custom">Custom</option>
|
||||
<option data-order="asc" data-field="comment" value="1" data-i18n="Title A-Z">Title A-Z</option>
|
||||
@@ -4185,6 +4186,7 @@
|
||||
</div>
|
||||
<input id="persona_search_bar" class="text_pole width100p flex1 margin0" type="search" data-i18n="[placeholder]Search..." placeholder="Search..." maxlength="100">
|
||||
<select id="persona_sort_order" class="margin0">
|
||||
<option value="search" data-i18n="Search" hidden>Search</option>
|
||||
<option value="asc">A-Z</option>
|
||||
<option value="desc">Z-A</option>
|
||||
</select>
|
||||
@@ -4580,6 +4582,7 @@
|
||||
<div id="rm_button_group_chats" title="Create New Chat Group" data-i18n="[title]Create New Chat Group" class="menu_button fa-solid fa-users-gear "></div>
|
||||
<input id="character_search_bar" class="text_pole width100p" type="search" data-i18n="[placeholder]Search..." placeholder="Search..." maxlength="100" />
|
||||
<select id="character_sort_order" title="Characters sorting order" data-i18n="[title]Characters sorting order">
|
||||
<option data-field="search" data-order="desc" data-i18n="Search" hidden>Search</option>
|
||||
<option data-field="name" data-order="asc" data-i18n="A-Z">A-Z</option>
|
||||
<option data-field="name" data-order="desc" data-i18n="Z-A">Z-A</option>
|
||||
<option data-field="create_date" data-order="desc" data-i18n="Newest">Newest</option>
|
||||
|
@@ -1310,7 +1310,6 @@ function getCharacterBlock(item, id) {
|
||||
async function printCharacters(fullRefresh = false) {
|
||||
const storageKey = 'Characters_PerPage';
|
||||
const listId = '#rm_print_characters_block';
|
||||
const entities = getEntitiesList({ doFilter: true });
|
||||
|
||||
let currentScrollTop = $(listId).scrollTop();
|
||||
|
||||
@@ -1320,10 +1319,15 @@ async function printCharacters(fullRefresh = false) {
|
||||
await delay(1);
|
||||
}
|
||||
|
||||
// Before printing the personas, we check if we should enable/disable search sorting
|
||||
verifyCharactersSearchSortRule();
|
||||
|
||||
// We are actually always reprinting filters, as it "doesn't hurt", and this way they are always up to date
|
||||
printTagFilters(tag_filter_types.character);
|
||||
printTagFilters(tag_filter_types.group_member);
|
||||
|
||||
const entities = getEntitiesList({ doFilter: true });
|
||||
|
||||
$('#rm_print_characters_pagination').pagination({
|
||||
dataSource: entities,
|
||||
pageSize: Number(localStorage.getItem(storageKey)) || per_page_default,
|
||||
@@ -1383,6 +1387,26 @@ async function printCharacters(fullRefresh = false) {
|
||||
favsToHotswap();
|
||||
}
|
||||
|
||||
/** Checks the state of the current search, and adds/removes the search sorting option accordingly */
|
||||
function verifyCharactersSearchSortRule() {
|
||||
const searchTerm = entitiesFilter.getFilterData(FILTER_TYPES.SEARCH);
|
||||
const searchOption = $('#character_sort_order option[data-field="search"]');
|
||||
const selector = $('#character_sort_order');
|
||||
const isHidden = searchOption.attr('hidden') !== undefined;
|
||||
|
||||
// If we have a search term, we are displaying the sorting option for it
|
||||
if (searchTerm && isHidden) {
|
||||
searchOption.removeAttr('hidden');
|
||||
searchOption.prop('selected', true);
|
||||
flashHighlight(selector);
|
||||
}
|
||||
// If search got cleared, we make sure to hide the option and go back to the one before
|
||||
if (!searchTerm && !isHidden) {
|
||||
searchOption.attr('hidden', '');
|
||||
$(`#character_sort_order option[data-order="${power_user.sort_order}"][data-field="${power_user.sort_field}"]`).prop('selected', true);
|
||||
}
|
||||
}
|
||||
|
||||
/** @typedef {object} Character - A character */
|
||||
/** @typedef {object} Group - A group */
|
||||
|
||||
@@ -1465,7 +1489,7 @@ export function getEntitiesList({ doFilter = false, doSort = true } = {}) {
|
||||
const subCount = subEntities.length;
|
||||
subEntities = filterByTagState(entities, { subForEntity: entity });
|
||||
if (doFilter) {
|
||||
subEntities = entitiesFilter.applyFilters(subEntities);
|
||||
subEntities = entitiesFilter.applyFilters(subEntities, { clearScoreCache: false });
|
||||
}
|
||||
entity.entities = subEntities;
|
||||
entity.hidden = subCount - subEntities.length;
|
||||
@@ -5914,17 +5938,16 @@ export async function getUserAvatars(doRender = true, openPageAt = '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
allEntities.sort((a, b) => {
|
||||
const aName = String(power_user.personas[a] || a);
|
||||
const bName = String(power_user.personas[b] || b);
|
||||
return power_user.persona_sort_order === 'asc' ? aName.localeCompare(bName) : bName.localeCompare(aName);
|
||||
});
|
||||
|
||||
if (!doRender) {
|
||||
return allEntities;
|
||||
}
|
||||
|
||||
const entities = personasFilter.applyFilters(allEntities);
|
||||
// Before printing the personas, we check if we should enable/disable search sorting
|
||||
verifyPersonaSearchSortRule();
|
||||
|
||||
let entities = personasFilter.applyFilters(allEntities);
|
||||
entities = sortPersonas(entities);
|
||||
|
||||
const storageKey = 'Personas_PerPage';
|
||||
const listId = '#user_avatar_block';
|
||||
const perPage = Number(localStorage.getItem(storageKey)) || 5;
|
||||
@@ -5978,6 +6001,50 @@ function highlightSelectedAvatar() {
|
||||
$(`#user_avatar_block .avatar-container[imgfile="${user_avatar}"]`).addClass('selected');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort the given personas
|
||||
* @param {string[]} personas - The persona names to sort
|
||||
* @returns {string[]} The sorted persona names arrray, same reference as passed in
|
||||
*/
|
||||
function sortPersonas(personas) {
|
||||
const option = $('#persona_sort_order').find(':selected');
|
||||
if (option.attr('value') === 'search') {
|
||||
personas.sort((a, b) => {
|
||||
const aScore = personasFilter.getScore(FILTER_TYPES.PERSONA_SEARCH, a);
|
||||
const bScore = personasFilter.getScore(FILTER_TYPES.PERSONA_SEARCH, b);
|
||||
return (aScore - bScore);
|
||||
});
|
||||
} else {
|
||||
personas.sort((a, b) => {
|
||||
const aName = String(power_user.personas[a] || a);
|
||||
const bName = String(power_user.personas[b] || b);
|
||||
return power_user.persona_sort_order === 'asc' ? aName.localeCompare(bName) : bName.localeCompare(aName);
|
||||
});
|
||||
}
|
||||
|
||||
return personas;
|
||||
}
|
||||
|
||||
/** Checks the state of the current search, and adds/removes the search sorting option accordingly */
|
||||
function verifyPersonaSearchSortRule() {
|
||||
const searchTerm = personasFilter.getFilterData(FILTER_TYPES.PERSONA_SEARCH);
|
||||
const searchOption = $('#persona_sort_order option[value="search"]');
|
||||
const selector = $('#persona_sort_order');
|
||||
const isHidden = searchOption.attr('hidden') !== undefined;
|
||||
|
||||
// If we have a search term, we are displaying the sorting option for it
|
||||
if (searchTerm && isHidden) {
|
||||
searchOption.removeAttr('hidden');
|
||||
selector.val(searchOption.attr('value'));
|
||||
flashHighlight(selector);
|
||||
}
|
||||
// If search got cleared, we make sure to hide the option and go back to the one before
|
||||
if (!searchTerm && !isHidden) {
|
||||
searchOption.attr('hidden', '');
|
||||
selector.val(power_user.persona_sort_order);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a rendered avatar block.
|
||||
* @param {string} name Avatar file name
|
||||
@@ -8690,7 +8757,7 @@ jQuery(async function () {
|
||||
entitiesFilter.setFilterData(FILTER_TYPES.SEARCH, searchQuery);
|
||||
});
|
||||
$('#character_search_bar').on('input', function () {
|
||||
const searchQuery = String($(this).val()).toLowerCase();
|
||||
const searchQuery = String($(this).val());
|
||||
debouncedCharacterSearch(searchQuery);
|
||||
});
|
||||
|
||||
@@ -8698,7 +8765,7 @@ jQuery(async function () {
|
||||
personasFilter.setFilterData(FILTER_TYPES.PERSONA_SEARCH, searchQuery);
|
||||
});
|
||||
$('#persona_search_bar').on('input', function () {
|
||||
const searchQuery = String($(this).val()).toLowerCase();
|
||||
const searchQuery = String($(this).val());
|
||||
debouncedPersonaSearch(searchQuery);
|
||||
});
|
||||
|
||||
|
@@ -1,9 +1,16 @@
|
||||
import { fuzzySearchCharacters, fuzzySearchGroups, fuzzySearchPersonas, fuzzySearchTags, fuzzySearchWorldInfo, power_user } from './power-user.js';
|
||||
import { tag_map } from './tags.js';
|
||||
import { includesIgnoreCaseAndAccents } from './utils.js';
|
||||
|
||||
|
||||
/**
|
||||
* @typedef FilterType The filter type possible for this filter helper
|
||||
* @type {'search'|'tag'|'folder'|'fav'|'group'|'world_info_search'|'persona_search'}
|
||||
*/
|
||||
|
||||
/**
|
||||
* The filter types
|
||||
* @type {{ SEARCH: string, TAG: string, FOLDER: string, FAV: string, GROUP: string, WORLD_INFO_SEARCH: string, PERSONA_SEARCH: string, [key: string]: string }}
|
||||
* @type {{ SEARCH: 'search', TAG: 'tag', FOLDER: 'folder', FAV: 'fav', GROUP: 'group', WORLD_INFO_SEARCH: 'world_info_search', PERSONA_SEARCH: 'persona_search'}}
|
||||
*/
|
||||
export const FILTER_TYPES = {
|
||||
SEARCH: 'search',
|
||||
@@ -56,12 +63,22 @@ export function isFilterState(a, b) {
|
||||
* data = filterHelper.applyFilters(data);
|
||||
*/
|
||||
export class FilterHelper {
|
||||
|
||||
/**
|
||||
* Cache fuzzy search weighting scores for re-usability, sorting and stuff
|
||||
*
|
||||
* Contains maps of weighting numbers assigned to their uid/id, for each of the different `FILTER_TYPES`
|
||||
* @type {Map<FilterType, Map<string|number,number>>}
|
||||
*/
|
||||
scoreCache;
|
||||
|
||||
/**
|
||||
* Creates a new FilterHelper
|
||||
* @param {Function} onDataChanged Callback to trigger when the filter data changes
|
||||
*/
|
||||
constructor(onDataChanged) {
|
||||
this.onDataChanged = onDataChanged;
|
||||
this.scoreCache = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,7 +152,10 @@ export class FilterHelper {
|
||||
}
|
||||
|
||||
const fuzzySearchResults = fuzzySearchWorldInfo(data, term);
|
||||
return data.filter(entity => fuzzySearchResults.includes(entity.uid));
|
||||
this.cacheScores(FILTER_TYPES.WORLD_INFO_SEARCH, new Map(fuzzySearchResults.map(i => [i.item?.uid, i.score])));
|
||||
|
||||
const filteredData = data.filter(entity => fuzzySearchResults.find(x => x.item === entity));
|
||||
return filteredData;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,7 +171,10 @@ export class FilterHelper {
|
||||
}
|
||||
|
||||
const fuzzySearchResults = fuzzySearchPersonas(data, term);
|
||||
return data.filter(entity => fuzzySearchResults.includes(entity));
|
||||
this.cacheScores(FILTER_TYPES.PERSONA_SEARCH, new Map(fuzzySearchResults.map(i => [i.item.key, i.score])));
|
||||
|
||||
const filteredData = data.filter(name => fuzzySearchResults.find(x => x.item.key === name));
|
||||
return filteredData;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -263,29 +286,28 @@ export class FilterHelper {
|
||||
return data;
|
||||
}
|
||||
|
||||
const searchValue = this.filterData[FILTER_TYPES.SEARCH].trim().toLowerCase();
|
||||
const fuzzySearchCharactersResults = power_user.fuzzy_search ? fuzzySearchCharacters(searchValue) : [];
|
||||
const fuzzySearchGroupsResults = power_user.fuzzy_search ? fuzzySearchGroups(searchValue) : [];
|
||||
const fuzzySearchTagsResult = power_user.fuzzy_search ? fuzzySearchTags(searchValue) : [];
|
||||
const searchValue = this.filterData[FILTER_TYPES.SEARCH];
|
||||
|
||||
// Save fuzzy search results and scores if enabled
|
||||
if (power_user.fuzzy_search) {
|
||||
const fuzzySearchCharactersResults = fuzzySearchCharacters(searchValue);
|
||||
const fuzzySearchGroupsResults = fuzzySearchGroups(searchValue);
|
||||
const fuzzySearchTagsResult = fuzzySearchTags(searchValue);
|
||||
this.cacheScores(FILTER_TYPES.SEARCH, new Map(fuzzySearchCharactersResults.map(i => [`character.${i.refIndex}`, i.score])));
|
||||
this.cacheScores(FILTER_TYPES.SEARCH, new Map(fuzzySearchGroupsResults.map(i => [`group.${i.item.id}`, i.score])));
|
||||
this.cacheScores(FILTER_TYPES.SEARCH, new Map(fuzzySearchTagsResult.map(i => [`tag.${i.item.id}`, i.score])));
|
||||
}
|
||||
|
||||
const _this = this;
|
||||
function getIsValidSearch(entity) {
|
||||
const isGroup = entity.type === 'group';
|
||||
const isCharacter = entity.type === 'character';
|
||||
const isTag = entity.type === 'tag';
|
||||
|
||||
if (power_user.fuzzy_search) {
|
||||
if (isCharacter) {
|
||||
return fuzzySearchCharactersResults.includes(parseInt(entity.id));
|
||||
} else if (isGroup) {
|
||||
return fuzzySearchGroupsResults.includes(String(entity.id));
|
||||
} else if (isTag) {
|
||||
return fuzzySearchTagsResult.includes(String(entity.id));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
// We can filter easily by checking if we have saved a score
|
||||
const score = _this.getScore(FILTER_TYPES.SEARCH, `${entity.type}.${entity.id}`);
|
||||
return score !== undefined;
|
||||
}
|
||||
else {
|
||||
return entity.item?.name?.toLowerCase()?.includes(searchValue) || false;
|
||||
// Compare insensitive and without accents
|
||||
return includesIgnoreCaseAndAccents(entity.item?.name, searchValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,7 +332,7 @@ export class FilterHelper {
|
||||
|
||||
/**
|
||||
* Gets the filter data for the given filter type.
|
||||
* @param {string} filterType The filter type to get data for.
|
||||
* @param {FilterType} filterType The filter type to get data for.
|
||||
*/
|
||||
getFilterData(filterType) {
|
||||
return this.filterData[filterType];
|
||||
@@ -318,11 +340,51 @@ export class FilterHelper {
|
||||
|
||||
/**
|
||||
* Applies all filters to the given data.
|
||||
* @param {any[]} data The data to filter.
|
||||
* @param {any[]} data - The data to filter.
|
||||
* @param {object} options - Optional call parameters
|
||||
* @param {boolean|FilterType} [options.clearScoreCache=true] - Whether the score
|
||||
* @returns {any[]} The filtered data.
|
||||
*/
|
||||
applyFilters(data) {
|
||||
applyFilters(data, { clearScoreCache = true } = {}) {
|
||||
if (clearScoreCache) this.clearScoreCache();
|
||||
return Object.values(this.filterFunctions)
|
||||
.reduce((data, fn) => fn(data), data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache scores for a specific filter type
|
||||
* @param {FilterType} type - The type of data being cached
|
||||
* @param {Map<string|number, number>} results - The search results containing mapped item identifiers and their scores
|
||||
*/
|
||||
cacheScores(type, results) {
|
||||
/** @type {Map<string|number, number>} */
|
||||
const typeScores = this.scoreCache.get(type) || new Map();
|
||||
for (const [uid, score] of results) {
|
||||
typeScores.set(uid, score);
|
||||
}
|
||||
this.scoreCache.set(type, typeScores);
|
||||
console.debug('search scores chached', type, typeScores);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cached score for an item by type and its identifier
|
||||
* @param {FilterType} type The type of data
|
||||
* @param {string|number} uid The unique identifier for an item
|
||||
* @returns {number|undefined} The cached score, or `undefined` if no score is present
|
||||
*/
|
||||
getScore(type, uid) {
|
||||
return this.scoreCache.get(type)?.get(uid) ?? undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the score cache for a specific type, or completely if no type is specified
|
||||
* @param {FilterType} [type] The type of data to clear scores for. Clears all if unspecified.
|
||||
*/
|
||||
clearScoreCache(type) {
|
||||
if (type) {
|
||||
this.scoreCache.set(type, new Map());
|
||||
} else {
|
||||
this.scoreCache = new Map();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -635,7 +635,9 @@ export function initPersonas() {
|
||||
$('#personas_restore').on('click', () => $('#personas_restore_input').trigger('click'));
|
||||
$('#personas_restore_input').on('change', onPersonasRestoreInput);
|
||||
$('#persona_sort_order').val(power_user.persona_sort_order).on('input', function () {
|
||||
power_user.persona_sort_order = String($(this).val());
|
||||
const value = String($(this).val());
|
||||
// Save sort order, but do not save search sorting, as this is a temporary sorting option
|
||||
if (value !== 'search') power_user.persona_sort_order = value;
|
||||
getUserAvatars(true, user_avatar);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
@@ -22,6 +22,7 @@ import {
|
||||
ANIMATION_DURATION_DEFAULT,
|
||||
setActiveGroup,
|
||||
setActiveCharacter,
|
||||
entitiesFilter,
|
||||
} from '../script.js';
|
||||
import { isMobile, initMovingUI, favsToHotswap } from './RossAscends-mods.js';
|
||||
import {
|
||||
@@ -35,12 +36,13 @@ import {
|
||||
} from './instruct-mode.js';
|
||||
|
||||
import { registerSlashCommand } from './slash-commands.js';
|
||||
import { tag_map, tags } from './tags.js';
|
||||
import { getTagsList, tag_map, tags } from './tags.js';
|
||||
import { tokenizers } from './tokenizers.js';
|
||||
import { BIAS_CACHE } from './logit-bias.js';
|
||||
import { renderTemplateAsync } from './templates.js';
|
||||
|
||||
import { countOccurrences, debounce, delay, download, getFileText, isOdd, resetScrollHeight, shuffle, sortMoments, stringToRange, timestampToMoment } from './utils.js';
|
||||
import { FILTER_TYPES } from './filters.js';
|
||||
|
||||
export {
|
||||
loadPowerUserSettings,
|
||||
@@ -1860,10 +1862,17 @@ function highlightDefaultContext() {
|
||||
$('#context_delete_preset').toggleClass('disabled', power_user.default_context === power_user.context.preset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fuzzy search characters by a search term
|
||||
* @param {string} searchValue - The search term
|
||||
* @returns {{item?: *, refIndex: number, score: number}[]} Results as items with their score
|
||||
*/
|
||||
export function fuzzySearchCharacters(searchValue) {
|
||||
// @ts-ignore
|
||||
const fuse = new Fuse(characters, {
|
||||
keys: [
|
||||
{ name: 'data.name', weight: 8 },
|
||||
{ name: 'data.name', weight: 20 },
|
||||
{ name: '#tags', weight: 10, getFn: (character) => getTagsList(character.avatar).map(x => x.name).join('||') },
|
||||
{ name: 'data.description', weight: 3 },
|
||||
{ name: 'data.mes_example', weight: 3 },
|
||||
{ name: 'data.scenario', weight: 2 },
|
||||
@@ -1876,82 +1885,114 @@ export function fuzzySearchCharacters(searchValue) {
|
||||
],
|
||||
includeScore: true,
|
||||
ignoreLocation: true,
|
||||
useExtendedSearch: true,
|
||||
threshold: 0.2,
|
||||
});
|
||||
|
||||
const results = fuse.search(searchValue);
|
||||
console.debug('Characters fuzzy search results for ' + searchValue, results);
|
||||
const indices = results.map(x => x.refIndex);
|
||||
return indices;
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fuzzy search world info entries by a search term
|
||||
* @param {*[]} data - WI items data array
|
||||
* @param {string} searchValue - The search term
|
||||
* @returns {{item?: *, refIndex: number, score: number}[]} Results as items with their score
|
||||
*/
|
||||
export function fuzzySearchWorldInfo(data, searchValue) {
|
||||
// @ts-ignore
|
||||
const fuse = new Fuse(data, {
|
||||
keys: [
|
||||
{ name: 'key', weight: 3 },
|
||||
{ name: 'key', weight: 20 },
|
||||
{ name: 'group', weight: 15 },
|
||||
{ name: 'comment', weight: 10 },
|
||||
{ name: 'keysecondary', weight: 10 },
|
||||
{ name: 'content', weight: 3 },
|
||||
{ name: 'comment', weight: 2 },
|
||||
{ name: 'keysecondary', weight: 2 },
|
||||
{ name: 'uid', weight: 1 },
|
||||
{ name: 'automationId', weight: 1 },
|
||||
],
|
||||
includeScore: true,
|
||||
ignoreLocation: true,
|
||||
useExtendedSearch: true,
|
||||
threshold: 0.2,
|
||||
});
|
||||
|
||||
const results = fuse.search(searchValue);
|
||||
console.debug('World Info fuzzy search results for ' + searchValue, results);
|
||||
return results.map(x => x.item?.uid);
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fuzzy search persona entries by a search term
|
||||
* @param {*[]} data - persona data array
|
||||
* @param {string} searchValue - The search term
|
||||
* @returns {{item?: *, refIndex: number, score: number}[]} Results as items with their score
|
||||
*/
|
||||
export function fuzzySearchPersonas(data, searchValue) {
|
||||
data = data.map(x => ({ key: x, description: power_user.persona_descriptions[x]?.description ?? '', name: power_user.personas[x] ?? '' }));
|
||||
data = data.map(x => ({ key: x, name: power_user.personas[x] ?? '', description: power_user.persona_descriptions[x]?.description ?? '' }));
|
||||
// @ts-ignore
|
||||
const fuse = new Fuse(data, {
|
||||
keys: [
|
||||
{ name: 'name', weight: 4 },
|
||||
{ name: 'description', weight: 1 },
|
||||
{ name: 'name', weight: 20 },
|
||||
{ name: 'description', weight: 3 },
|
||||
],
|
||||
includeScore: true,
|
||||
ignoreLocation: true,
|
||||
useExtendedSearch: true,
|
||||
threshold: 0.2,
|
||||
});
|
||||
|
||||
const results = fuse.search(searchValue);
|
||||
console.debug('Personas fuzzy search results for ' + searchValue, results);
|
||||
return results.map(x => x.item?.key);
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fuzzy search tags by a search term
|
||||
* @param {string} searchValue - The search term
|
||||
* @returns {{item?: *, refIndex: number, score: number}[]} Results as items with their score
|
||||
*/
|
||||
export function fuzzySearchTags(searchValue) {
|
||||
// @ts-ignore
|
||||
const fuse = new Fuse(tags, {
|
||||
keys: [
|
||||
{ name: 'name', weight: 1 },
|
||||
],
|
||||
includeScore: true,
|
||||
ignoreLocation: true,
|
||||
useExtendedSearch: true,
|
||||
threshold: 0.2,
|
||||
});
|
||||
|
||||
const results = fuse.search(searchValue);
|
||||
console.debug('Tags fuzzy search results for ' + searchValue, results);
|
||||
const ids = results.map(x => String(x.item?.id)).filter(x => x);
|
||||
return ids;
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fuzzy search groups by a search term
|
||||
* @param {string} searchValue - The search term
|
||||
* @returns {{item?: *, refIndex: number, score: number}[]} Results as items with their score
|
||||
*/
|
||||
export function fuzzySearchGroups(searchValue) {
|
||||
// @ts-ignore
|
||||
const fuse = new Fuse(groups, {
|
||||
keys: [
|
||||
{ name: 'name', weight: 3 },
|
||||
{ name: 'members', weight: 1 },
|
||||
{ name: 'name', weight: 20 },
|
||||
{ name: 'members', weight: 15 },
|
||||
{ name: '#tags', weight: 10, getFn: (group) => getTagsList(group.id).map(x => x.name).join('||') },
|
||||
{ name: 'id', weight: 1 },
|
||||
],
|
||||
includeScore: true,
|
||||
ignoreLocation: true,
|
||||
useExtendedSearch: true,
|
||||
threshold: 0.2,
|
||||
});
|
||||
|
||||
const results = fuse.search(searchValue);
|
||||
console.debug('Groups fuzzy search results for ' + searchValue, results);
|
||||
const ids = results.map(x => String(x.item?.id)).filter(x => x);
|
||||
return ids;
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2026,15 +2067,24 @@ function sortEntitiesList(entities) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isSearch = $('#character_sort_order option[data-field="search"]').is(':selected');
|
||||
|
||||
entities.sort((a, b) => {
|
||||
// Sort tags/folders will always be at the top
|
||||
if (a.type === 'tag' && b.type !== 'tag') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (a.type !== 'tag' && b.type === 'tag') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// If we have search sorting, we take scores and use those
|
||||
if (isSearch) {
|
||||
const aScore = entitiesFilter.getScore(FILTER_TYPES.SEARCH, `${a.type}.${a.id}`);
|
||||
const bScore = entitiesFilter.getScore(FILTER_TYPES.SEARCH, `${b.type}.${b.id}`);
|
||||
return (aScore - bScore);
|
||||
}
|
||||
|
||||
return sortFunc(a.item, b.item);
|
||||
});
|
||||
}
|
||||
@@ -2745,6 +2795,7 @@ function setAvgBG() {
|
||||
}
|
||||
|
||||
async function setThemeCallback(_, text) {
|
||||
// @ts-ignore
|
||||
const fuse = new Fuse(themes, {
|
||||
keys: [
|
||||
{ name: 'name', weight: 1 },
|
||||
@@ -2767,6 +2818,7 @@ async function setThemeCallback(_, text) {
|
||||
}
|
||||
|
||||
async function setmovingUIPreset(_, text) {
|
||||
// @ts-ignore
|
||||
const fuse = new Fuse(movingUIPresets, {
|
||||
keys: [
|
||||
{ name: 'name', weight: 1 },
|
||||
@@ -3208,9 +3260,13 @@ $(document).ready(() => {
|
||||
});
|
||||
|
||||
$('#character_sort_order').on('change', function () {
|
||||
power_user.sort_field = $(this).find(':selected').data('field');
|
||||
power_user.sort_order = $(this).find(':selected').data('order');
|
||||
power_user.sort_rule = $(this).find(':selected').data('rule');
|
||||
const field = String($(this).find(':selected').data('field'));
|
||||
// Save sort order, but do not save search sorting, as this is a temporary sorting option
|
||||
if (field !== 'search') {
|
||||
power_user.sort_field = field;
|
||||
power_user.sort_order = $(this).find(':selected').data('order');
|
||||
power_user.sort_rule = $(this).find(':selected').data('rule');
|
||||
}
|
||||
printCharactersDebounced();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
@@ -1430,3 +1430,22 @@ export function flashHighlight(element, timespan = 2000) {
|
||||
element.addClass('flash animated');
|
||||
setTimeout(() => element.removeClass('flash animated'), timespan);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a case-insensitive and accent-insensitive substring search.
|
||||
* This function normalizes the strings to remove diacritical marks and converts them to lowercase to ensure the search is insensitive to case and accents.
|
||||
*
|
||||
* @param {string} text - The text in which to search for the substring.
|
||||
* @param {string} searchTerm - The substring to search for in the text.
|
||||
* @returns {boolean} - Returns true if the searchTerm is found within the text, otherwise returns false.
|
||||
*/
|
||||
export function includesIgnoreCaseAndAccents(text, searchTerm) {
|
||||
if (!text || !searchTerm) return false; // Return false if either string is empty
|
||||
|
||||
// Normalize and remove diacritics, then convert to lower case
|
||||
const normalizedText = text.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
|
||||
const normalizedSearchTerm = searchTerm.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
|
||||
|
||||
// Check if the normalized text includes the normalized search term
|
||||
return normalizedText.includes(normalizedSearchTerm);
|
||||
}
|
||||
|
@@ -679,7 +679,17 @@ function sortEntries(data) {
|
||||
const sortRule = option.data('rule');
|
||||
const orderSign = sortOrder === 'asc' ? 1 : -1;
|
||||
|
||||
if (sortRule === 'custom') {
|
||||
if (!data.length) return data;
|
||||
|
||||
// If we have a search term for WI, we are sorting by weighting scores
|
||||
if (sortRule === 'search') {
|
||||
data.sort((a, b) => {
|
||||
const aScore = worldInfoFilter.getScore(FILTER_TYPES.WORLD_INFO_SEARCH, a.uid);
|
||||
const bScore = worldInfoFilter.getScore(FILTER_TYPES.WORLD_INFO_SEARCH, b.uid);
|
||||
return (aScore - bScore);
|
||||
});
|
||||
}
|
||||
else if (sortRule === 'custom') {
|
||||
// First by display index, then by order, then by uid
|
||||
data.sort((a, b) => {
|
||||
const aValue = a.displayIndex;
|
||||
@@ -762,6 +772,9 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Before printing the WI, we check if we should enable/disable search sorting
|
||||
verifyWorldInfoSearchSortRule();
|
||||
|
||||
function getDataArray(callback) {
|
||||
// Convert the data.entries object into an array
|
||||
let entriesArray = Object.keys(data.entries).map(uid => {
|
||||
@@ -770,10 +783,11 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
|
||||
return entry;
|
||||
});
|
||||
|
||||
// Sort the entries array by displayIndex and uid
|
||||
entriesArray.sort((a, b) => a.displayIndex - b.displayIndex || a.uid - b.uid);
|
||||
entriesArray = sortEntries(entriesArray);
|
||||
// Apply the filter and do the chosen sorting
|
||||
entriesArray = worldInfoFilter.applyFilters(entriesArray);
|
||||
entriesArray = sortEntries(entriesArray)
|
||||
|
||||
// Run the callback for printing this
|
||||
typeof callback === 'function' && callback(entriesArray);
|
||||
return entriesArray;
|
||||
}
|
||||
@@ -1008,6 +1022,26 @@ const originalDataKeyMap = {
|
||||
'groupOverride': 'extensions.group_override',
|
||||
};
|
||||
|
||||
/** Checks the state of the current search, and adds/removes the search sorting option accordingly */
|
||||
function verifyWorldInfoSearchSortRule() {
|
||||
const searchTerm = worldInfoFilter.getFilterData(FILTER_TYPES.WORLD_INFO_SEARCH);
|
||||
const searchOption = $('#world_info_sort_order option[data-rule="search"]');
|
||||
const selector = $('#world_info_sort_order');
|
||||
const isHidden = searchOption.attr('hidden') !== undefined;
|
||||
|
||||
// If we have a search term, we are displaying the sorting option for it
|
||||
if (searchTerm && isHidden) {
|
||||
searchOption.removeAttr('hidden');
|
||||
selector.val(searchOption.attr('value') || '0');
|
||||
flashHighlight(selector);
|
||||
}
|
||||
// If search got cleared, we make sure to hide the option and go back to the one before
|
||||
if (!searchTerm && !isHidden) {
|
||||
searchOption.attr('hidden', '');
|
||||
selector.val(localStorage.getItem(SORT_ORDER_KEY) || '0');
|
||||
}
|
||||
}
|
||||
|
||||
function setOriginalDataValue(data, uid, key, value) {
|
||||
if (data.originalData && Array.isArray(data.originalData.entries)) {
|
||||
let originalEntry = data.originalData.entries.find(x => x.uid === uid);
|
||||
@@ -3065,7 +3099,8 @@ jQuery(() => {
|
||||
|
||||
$('#world_info_sort_order').on('change', function () {
|
||||
const value = String($(this).find(':selected').val());
|
||||
localStorage.setItem(SORT_ORDER_KEY, value);
|
||||
// Save sort order, but do not save search sorting, as this is a temporary sorting option
|
||||
if (value !== 'search') localStorage.setItem(SORT_ORDER_KEY, value);
|
||||
updateEditor(navigation_option.none);
|
||||
});
|
||||
|
||||
|
Reference in New Issue
Block a user