mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Scored search sorting for char list
This commit is contained in:
@ -4582,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>
|
<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" />
|
<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">
|
<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="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="name" data-order="desc" data-i18n="Z-A">Z-A</option>
|
||||||
<option data-field="create_date" data-order="desc" data-i18n="Newest">Newest</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) {
|
async function printCharacters(fullRefresh = false) {
|
||||||
const storageKey = 'Characters_PerPage';
|
const storageKey = 'Characters_PerPage';
|
||||||
const listId = '#rm_print_characters_block';
|
const listId = '#rm_print_characters_block';
|
||||||
const entities = getEntitiesList({ doFilter: true });
|
|
||||||
|
|
||||||
let currentScrollTop = $(listId).scrollTop();
|
let currentScrollTop = $(listId).scrollTop();
|
||||||
|
|
||||||
@ -1320,10 +1319,15 @@ async function printCharacters(fullRefresh = false) {
|
|||||||
await delay(1);
|
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
|
// 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.character);
|
||||||
printTagFilters(tag_filter_types.group_member);
|
printTagFilters(tag_filter_types.group_member);
|
||||||
|
|
||||||
|
const entities = getEntitiesList({ doFilter: true });
|
||||||
|
|
||||||
$('#rm_print_characters_pagination').pagination({
|
$('#rm_print_characters_pagination').pagination({
|
||||||
dataSource: entities,
|
dataSource: entities,
|
||||||
pageSize: Number(localStorage.getItem(storageKey)) || per_page_default,
|
pageSize: Number(localStorage.getItem(storageKey)) || per_page_default,
|
||||||
@ -1383,6 +1387,26 @@ async function printCharacters(fullRefresh = false) {
|
|||||||
favsToHotswap();
|
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} Character - A character */
|
||||||
/** @typedef {object} Group - A group */
|
/** @typedef {object} Group - A group */
|
||||||
|
|
||||||
@ -8733,7 +8757,7 @@ jQuery(async function () {
|
|||||||
entitiesFilter.setFilterData(FILTER_TYPES.SEARCH, searchQuery);
|
entitiesFilter.setFilterData(FILTER_TYPES.SEARCH, searchQuery);
|
||||||
});
|
});
|
||||||
$('#character_search_bar').on('input', function () {
|
$('#character_search_bar').on('input', function () {
|
||||||
const searchQuery = String($(this).val()).toLowerCase();
|
const searchQuery = String($(this).val());
|
||||||
debouncedCharacterSearch(searchQuery);
|
debouncedCharacterSearch(searchQuery);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -285,29 +285,28 @@ export class FilterHelper {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchValue = this.filterData[FILTER_TYPES.SEARCH].trim().toLowerCase();
|
const searchValue = this.filterData[FILTER_TYPES.SEARCH];
|
||||||
const fuzzySearchCharactersResults = power_user.fuzzy_search ? fuzzySearchCharacters(searchValue) : [];
|
|
||||||
const fuzzySearchGroupsResults = power_user.fuzzy_search ? fuzzySearchGroups(searchValue) : [];
|
|
||||||
const fuzzySearchTagsResult = power_user.fuzzy_search ? fuzzySearchTags(searchValue) : [];
|
|
||||||
|
|
||||||
|
// 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) {
|
function getIsValidSearch(entity) {
|
||||||
const isGroup = entity.type === 'group';
|
|
||||||
const isCharacter = entity.type === 'character';
|
|
||||||
const isTag = entity.type === 'tag';
|
|
||||||
|
|
||||||
if (power_user.fuzzy_search) {
|
if (power_user.fuzzy_search) {
|
||||||
if (isCharacter) {
|
// We can filter easily by checking if we have saved a score
|
||||||
return fuzzySearchCharactersResults.includes(parseInt(entity.id));
|
const score = _this.getScore(FILTER_TYPES.SEARCH, `${entity.type}.${entity.id}`);
|
||||||
} else if (isGroup) {
|
return score !== undefined;
|
||||||
return fuzzySearchGroupsResults.includes(String(entity.id));
|
|
||||||
} else if (isTag) {
|
|
||||||
return fuzzySearchTagsResult.includes(String(entity.id));
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return entity.item?.name?.toLowerCase()?.includes(searchValue) || false;
|
// Compare insensitive and without accents
|
||||||
|
return entity.item?.name?.localeCompare(searchValue, undefined, { sensitivity: 'base' }) === 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -370,7 +369,7 @@ export class FilterHelper {
|
|||||||
* Get the cached score for an item by type and its identifier
|
* Get the cached score for an item by type and its identifier
|
||||||
* @param {FilterType} type The type of data
|
* @param {FilterType} type The type of data
|
||||||
* @param {string|number} uid The unique identifier for an item
|
* @param {string|number} uid The unique identifier for an item
|
||||||
* @returns {number|undefined} The cached score, or undefined if no score is present
|
* @returns {number|undefined} The cached score, or `undefined` if no score is present
|
||||||
*/
|
*/
|
||||||
getScore(type, uid) {
|
getScore(type, uid) {
|
||||||
return this.scoreCache.get(type)?.get(uid) ?? undefined;
|
return this.scoreCache.get(type)?.get(uid) ?? undefined;
|
||||||
|
@ -22,6 +22,7 @@ import {
|
|||||||
ANIMATION_DURATION_DEFAULT,
|
ANIMATION_DURATION_DEFAULT,
|
||||||
setActiveGroup,
|
setActiveGroup,
|
||||||
setActiveCharacter,
|
setActiveCharacter,
|
||||||
|
entitiesFilter,
|
||||||
} from '../script.js';
|
} from '../script.js';
|
||||||
import { isMobile, initMovingUI, favsToHotswap } from './RossAscends-mods.js';
|
import { isMobile, initMovingUI, favsToHotswap } from './RossAscends-mods.js';
|
||||||
import {
|
import {
|
||||||
@ -41,6 +42,7 @@ import { BIAS_CACHE } from './logit-bias.js';
|
|||||||
import { renderTemplateAsync } from './templates.js';
|
import { renderTemplateAsync } from './templates.js';
|
||||||
|
|
||||||
import { countOccurrences, debounce, delay, download, getFileText, isOdd, resetScrollHeight, shuffle, sortMoments, stringToRange, timestampToMoment } from './utils.js';
|
import { countOccurrences, debounce, delay, download, getFileText, isOdd, resetScrollHeight, shuffle, sortMoments, stringToRange, timestampToMoment } from './utils.js';
|
||||||
|
import { FILTER_TYPES } from './filters.js';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
loadPowerUserSettings,
|
loadPowerUserSettings,
|
||||||
@ -1860,6 +1862,11 @@ function highlightDefaultContext() {
|
|||||||
$('#context_delete_preset').toggleClass('disabled', power_user.default_context === power_user.context.preset);
|
$('#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) {
|
export function fuzzySearchCharacters(searchValue) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const fuse = new Fuse(characters, {
|
const fuse = new Fuse(characters, {
|
||||||
@ -1882,8 +1889,7 @@ export function fuzzySearchCharacters(searchValue) {
|
|||||||
|
|
||||||
const results = fuse.search(searchValue);
|
const results = fuse.search(searchValue);
|
||||||
console.debug('Characters fuzzy search results for ' + searchValue, results);
|
console.debug('Characters fuzzy search results for ' + searchValue, results);
|
||||||
const indices = results.map(x => x.refIndex);
|
return results;
|
||||||
return indices;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1936,6 +1942,11 @@ export function fuzzySearchPersonas(data, searchValue) {
|
|||||||
return results;
|
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) {
|
export function fuzzySearchTags(searchValue) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const fuse = new Fuse(tags, {
|
const fuse = new Fuse(tags, {
|
||||||
@ -1949,10 +1960,14 @@ export function fuzzySearchTags(searchValue) {
|
|||||||
|
|
||||||
const results = fuse.search(searchValue);
|
const results = fuse.search(searchValue);
|
||||||
console.debug('Tags fuzzy search results for ' + searchValue, results);
|
console.debug('Tags fuzzy search results for ' + searchValue, results);
|
||||||
const ids = results.map(x => String(x.item?.id)).filter(x => x);
|
return results;
|
||||||
return ids;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
export function fuzzySearchGroups(searchValue) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const fuse = new Fuse(groups, {
|
const fuse = new Fuse(groups, {
|
||||||
@ -1967,8 +1982,7 @@ export function fuzzySearchGroups(searchValue) {
|
|||||||
|
|
||||||
const results = fuse.search(searchValue);
|
const results = fuse.search(searchValue);
|
||||||
console.debug('Groups fuzzy search results for ' + searchValue, results);
|
console.debug('Groups fuzzy search results for ' + searchValue, results);
|
||||||
const ids = results.map(x => String(x.item?.id)).filter(x => x);
|
return results;
|
||||||
return ids;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -2043,15 +2057,24 @@ function sortEntitiesList(entities) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isSearch = $('#character_sort_order option[data-field="search"]').is(':selected');
|
||||||
|
|
||||||
entities.sort((a, b) => {
|
entities.sort((a, b) => {
|
||||||
|
// Sort tags/folders will always be at the top
|
||||||
if (a.type === 'tag' && b.type !== 'tag') {
|
if (a.type === 'tag' && b.type !== 'tag') {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (a.type !== 'tag' && b.type === 'tag') {
|
if (a.type !== 'tag' && b.type === 'tag') {
|
||||||
return 1;
|
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);
|
return sortFunc(a.item, b.item);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -3227,9 +3250,13 @@ $(document).ready(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$('#character_sort_order').on('change', function () {
|
$('#character_sort_order').on('change', function () {
|
||||||
power_user.sort_field = $(this).find(':selected').data('field');
|
const field = String($(this).find(':selected').data('field'));
|
||||||
power_user.sort_order = $(this).find(':selected').data('order');
|
// Save sort order, but do not save search sorting, as this is a temporary sorting option
|
||||||
power_user.sort_rule = $(this).find(':selected').data('rule');
|
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();
|
printCharactersDebounced();
|
||||||
saveSettingsDebounced();
|
saveSettingsDebounced();
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user