Encapsulate logic into filters instead of spreading around

1395c0b8c6e4c1a59192051ecd72cd509dcdf709

Joe <joenunezb@gmail.com>

4 files changed, +84 -162Ignore whitespace
docs/fuzzy_search_flow.md+0 -113
@@ -1,113 +0,0 @@
1-## Input Sources to printCharacters
2-
3-`printCharacters` is the main function that triggers the fuzzy search process if fuzzy search is enabled.
4-
5-```mermaid
6-%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 50}}}%%
7-flowchart TD
8- subgraph User Actions
9- UA1[Character Search Input]
10- UA2[Tag Filter Click]
11- UA3[Folder Navigation]
12- UA4[Character Delete]
13- UA5[Character Create]
14- UA6[Character Import]
15- UA7[Clear All Filters]
16- UA8[Bulk Edit Operations]
17- UA9[Persona Changes]
18- end
19-
20- subgraph API Events
21- API1[Character List Update]
22- API2[Group Update]
23- API3[Tag Update]
24- end
25-
26- subgraph System Events
27- SE1[Page Load]
28- SE2[Content Manager Update]
29- SE3[Extension Events]
30- end
31-
32- UA1 -->|triggers| PCD[printCharactersDebounced]
33- UA2 -->|triggers| PCD
34- UA7 -->|triggers| PCD
35- UA8 -->|triggers| PCD
36- UA9 -->|triggers| PCD
37-
38- UA3 -->|triggers| PC[printCharacters]
39- UA4 -->|triggers| PC
40- UA5 -->|triggers| PC
41- UA6 -->|triggers| PC
42-
43- API1 -->|triggers| PC
44- API2 -->|triggers| PC
45- API3 -->|triggers| PC
46-
47- SE1 -->|triggers| PC
48- SE2 -->|triggers| PC
49- SE3 -->|triggers| PC
50-
51- PCD -->|debounced call| PC
52-
53- style PC fill:#f96,stroke:#333
54- style PCD fill:#f96,stroke:#333
55-```
56-
57-This diagram shows how `printCharacters` is called throughout the application:
58-
59-1. User Actions that trigger character list updates:
60- - Search input (debounced)
61- - Tag filter clicks (debounced)
62- - Folder navigation (direct)
63- - Character management operations (direct)
64-
65-2. API Events that require list refresh:
66- - Character list updates
67- - Group updates
68- - Tag system updates
69-
70-3. System Events:
71- - Initial page load
72- - Content manager updates
73- - Extension-triggered refreshes
74-
75-
76-
77-## Fuzzy Search Flow
78-
79-
80-This diagram shows the flow of fuzzy search operations:
81-```mermaid
82-sequenceDiagram
83- participant Data as Data Sources
84- participant PC as printCharacters
85- participant GEL as getEntitiesList
86- participant FH as FilterHelper
87- participant AF as applyFilters
88- participant FS as FuzzySearch Functions
89- participant Cache as FuzzySearchCaches
90-
91- Note over Data: Changes from:<br/>- Tags<br/>- Personas<br/>- World Info<br/>- Groups
92-
93- Data->>PC: All changes trigger printCharacters<br/>(direct or debounced)
94-
95- PC->>GEL: Call with {doFilter: true}
96- GEL->>FH: filterByTagState(entities)
97- GEL->>AF: entitiesFilter.applyFilters(entities)
98-
99- AF->>FH: Check scoreCache for existing results
100- FH-->>AF: Return cached scores if exist
101-
102- Note over FS: Filter functions include:<br/>SEARCH, <br/>FAV, <br/>GROUP, <br/>FOLDER, <br/>TAG, <br/>WORLD_INFO_SEARCH, <br/>PERSONA_SEARCH
103- AF->>FS: fuzzySearchCharacters/Groups/Tags
104- FS->>Cache: Check/Store results
105-
106- FS-->>AF: Return search results
107- AF->>FH: Cache new scores
108- AF-->>GEL: Return filtered entities
109- GEL-->>PC: Return final entities list
110-
111- PC->>Cache: clearFuzzySearchCaches()
112- Note over Cache: Cache is cleared at the end of<br/>each printCharacters call,<br/>ensuring fresh results for next search
113-```
public/script.js+5 -5
@@ -268,7 +268,6 @@ import { initServerHistory } from './scripts/server-history.js';
268268import { initSettingsSearch } from './scripts/setting-search.js';
269269import { initBulkEdit } from './scripts/bulk-edit.js';
270270import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';
271-import { clearFuzzySearchCaches } from './scripts/power-user.js';
272271
273272//exporting functions and vars for mods
274273export {
@@ -1516,7 +1515,6 @@ export async function printCharacters(fullRefresh = false) {
15161515 });
15171516
15181517 favsToHotswap();
1519- clearFuzzySearchCaches();
15201518}
15211519
15221520/** Checks the state of the current search, and adds/removes the search sorting option accordingly */
@@ -1623,7 +1621,7 @@ export function getEntitiesList({ doFilter = false, doSort = true } = {}) {
16231621 subEntities = filterByTagState(entities, { subForEntity: entity });
16241622 if (doFilter) {
16251623 // sub entities filter "hacked" because folder filter should not be applied there, so even in "only folders" mode characters show up
16261624 subEntities = entitiesFilter.applyFilters(subEntities, { clearScoreCache: false, tempOverrides: { [FILTER_TYPES.FOLDER]: FILTER_STATES.UNDEFINED }, clearFuzzySearchCaches: false });
16271625 }
16281626 if (doSort) {
16291627 sortEntitiesList(subEntities);
@@ -1636,11 +1634,11 @@ export function getEntitiesList({ doFilter = false, doSort = true } = {}) {
16361634 // Second run filters, hiding whatever should be filtered later
16371635 if (doFilter) {
16381636 const beforeFinalEntities = filterByTagState(entities, { globalDisplayFilters: true });
16391637 entities = entitiesFilter.applyFilters(beforeFinalEntities, { clearFuzzySearchCaches: false });
16401638
16411639 // Magic for folder filter. If that one is enabled, and no folders are display anymore, we remove that filter to actually show the characters.
16421640 if (isFilterState(entitiesFilter.getFilterData(FILTER_TYPES.FOLDER), FILTER_STATES.SELECTED) && entities.filter(x => x.type == 'tag').length == 0) {
16431641 entities = entitiesFilter.applyFilters(beforeFinalEntities, { tempOverrides: { [FILTER_TYPES.FOLDER]: FILTER_STATES.UNDEFINED }, clearFuzzySearchCaches: false });
16441642 }
16451643 }
16461644
@@ -1656,6 +1654,7 @@ export function getEntitiesList({ doFilter = false, doSort = true } = {}) {
16561654 if (doSort) {
16571655 sortEntitiesList(entities);
16581656 }
1657+ entitiesFilter.clearFuzzySearchCaches();
16591658 return entities;
16601659}
16611660
@@ -1751,6 +1750,7 @@ export async function getCharacters() {
17511750 }
17521751
17531752 await getGroups();
1753+ // clearFuzzySearchCaches();
17541754 await printCharacters(true);
17551755 }
17561756}
public/scripts/filters.js+51 -6
@@ -56,6 +56,19 @@ export function isFilterState(a, b) {
5656}
5757
5858/**
59+ * The fuzzy search categories
60+ * @type {{ characters: string, worldInfo: string, personas: string, tags: string, groups: string }}
61+ */
62+export const fuzzySearchCategories = Object.freeze({
63+ characters: 'characters',
64+ worldInfo: 'worldInfo',
65+ personas: 'personas',
66+ tags: 'tags',
67+ groups: 'groups',
68+});
69+
70+
71+/**
5972 * Helper class for filtering data.
6073 * @example
6174 * const filterHelper = new FilterHelper(() => console.log('data changed'));
@@ -73,12 +86,25 @@ export class FilterHelper {
7386 scoreCache;
7487
7588 /**
89+ * Cache for fuzzy search results per category.
90+ * @type {Object.<string, { resultMap: Map<string, any> }>}
91+ */
92+ fuzzySearchCaches;
93+
94+ /**
7695 * Creates a new FilterHelper
7796 * @param {Function} onDataChanged Callback to trigger when the filter data changes
7897 */
7998 constructor(onDataChanged) {
8099 this.onDataChanged = onDataChanged;
81100 this.scoreCache = new Map();
101+ this.fuzzySearchCaches = {
102+ [fuzzySearchCategories.characters]: { resultMap: new Map() },
103+ [fuzzySearchCategories.worldInfo]: { resultMap: new Map() },
104+ [fuzzySearchCategories.personas]: { resultMap: new Map() },
105+ [fuzzySearchCategories.tags]: { resultMap: new Map() },
106+ [fuzzySearchCategories.groups]: { resultMap: new Map() },
107+ };
82108 }
83109
84110 /**
@@ -151,7 +177,7 @@ export class FilterHelper {
151177 return data;
152178 }
153179
154180 const fuzzySearchResults = fuzzySearchWorldInfo(data, term, this.fuzzySearchCaches);
155181 this.cacheScores(FILTER_TYPES.WORLD_INFO_SEARCH, new Map(fuzzySearchResults.map(i => [i.item?.uid, i.score])));
156182
157183 const filteredData = data.filter(entity => fuzzySearchResults.find(x => x.item === entity));
@@ -170,7 +196,7 @@ export class FilterHelper {
170196 return data;
171197 }
172198
173199 const fuzzySearchResults = fuzzySearchPersonas(data, term, this.fuzzySearchCaches);
174200 this.cacheScores(FILTER_TYPES.PERSONA_SEARCH, new Map(fuzzySearchResults.map(i => [i.item.key, i.score])));
175201
176202 const filteredData = data.filter(name => fuzzySearchResults.find(x => x.item.key === name));
@@ -289,9 +315,9 @@ export class FilterHelper {
289315
290316 // Save fuzzy search results and scores if enabled
291317 if (power_user.fuzzy_search) {
292318 const fuzzySearchCharactersResults = fuzzySearchCharacters(searchValue, this.fuzzySearchCaches);
293319 const fuzzySearchGroupsResults = fuzzySearchGroups(searchValue, this.fuzzySearchCaches);
294320 const fuzzySearchTagsResult = fuzzySearchTags(searchValue, this.fuzzySearchCaches);
295321 this.cacheScores(FILTER_TYPES.SEARCH, new Map(fuzzySearchCharactersResults.map(i => [`character.${i.refIndex}`, i.score])));
296322 this.cacheScores(FILTER_TYPES.SEARCH, new Map(fuzzySearchGroupsResults.map(i => [`group.${i.item.id}`, i.score])));
297323 this.cacheScores(FILTER_TYPES.SEARCH, new Map(fuzzySearchTagsResult.map(i => [`tag.${i.item.id}`, i.score])));
@@ -343,11 +369,14 @@ export class FilterHelper {
343369 * @param {object} options - Optional call parameters
344370 * @param {boolean} [options.clearScoreCache=true] - Whether the score cache should be cleared.
345371 * @param {Object.<FilterType, any>} [options.tempOverrides={}] - Temporarily override specific filters for this filter application
372+ * @param {boolean} [options.clearFuzzySearchCaches=true] - Whether the fuzzy search caches should be cleared.
346373 * @returns {any[]} The filtered data.
347374 */
348375 applyFilters(data, { clearScoreCache = true, tempOverrides = {}, clearFuzzySearchCaches = true } = {}) {
349376 if (clearScoreCache) this.clearScoreCache();
350377
378+ if (clearFuzzySearchCaches) this.clearFuzzySearchCaches();
379+
351380 // Save original filter states
352381 const originalStates = {};
353382 for (const key in tempOverrides) {
@@ -411,4 +440,20 @@ export class FilterHelper {
411440 this.scoreCache = new Map();
412441 }
413442 }
443+
444+ /**
445+ * Clears fuzzy search caches
446+ * @param {keyof typeof fuzzySearchCategories} [type] Optional cache type to clear. If not provided, clears all caches
447+ */
448+ clearFuzzySearchCaches(type = null) {
449+ if (type && this.fuzzySearchCaches[type]) {
450+ this.fuzzySearchCaches[type].resultMap.clear();
451+ console.log(`Fuzzy search cache cleared for: ${type}`);
452+ } else {
453+ for (const cache of Object.values(this.fuzzySearchCaches)) {
454+ cache.resultMap.clear();
455+ }
456+ console.log('All fuzzy search caches cleared');
457+ }
458+ }
414459}
public/scripts/power-user.js+28 -38
@@ -53,6 +53,7 @@ import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandE
5353import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
5454import { POPUP_TYPE, callGenericPopup } from './popup.js';
5555import { loadSystemPrompts } from './sysprompt.js';
56+import { fuzzySearchCategories } from './filters.js';
5657
5758export {
5859 loadPowerUserSettings,
@@ -328,22 +329,6 @@ const contextControls = [
328329let browser_has_focus = true;
329330const debug_functions = [];
330331
331-const fuzzySearchCaches = {
332- characters: { resultMap: new Map() },
333- worldInfo: { resultMap: new Map() },
334- personas: { resultMap: new Map() },
335- tags: { resultMap: new Map() },
336- groups: { resultMap: new Map() },
337-};
338-
339-const fuzzySearchCategories = {
340- characters: 'characters',
341- worldInfo: 'worldInfo',
342- personas: 'personas',
343- tags: 'tags',
344- groups: 'groups',
345-};
346-
347332const setHotswapsDebounced = debounce(favsToHotswap);
348333
349334function playMessageSound() {
@@ -1845,19 +1830,26 @@ async function loadContextSettings() {
18451830 });
18461831}
18471832
1833+
18481834/**
18491835 * Common function to perform fuzzy search with caching
18501836 * @param {string} type - Type of search from fuzzySearchCategories
18511837 * @param {any[]} data - Data array to search in
18521838 * @param {Array<{name: string, weight: number, getFn?: Function}>} keys - Fuse.js keys configuration
18531839 * @param {string} searchValue - The search term
1840+ * @param {Object.<string, { resultMap: Map<string, any> }>} fuzzySearchCaches - Fuzzy search caches
18541841 * @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score
18551842 */
18561843function performFuzzySearch(type, data, keys, searchValue, fuzzySearchCaches) {
1844+ let startTime = performance.now();
18571845 const cache = fuzzySearchCaches[type];
18581846
18591847 // Check cache for existing results
18601848 if (cache.resultMap.has(searchValue)) {
1849+ let endTime = performance.now();
1850+ if (endTime - startTime > 1.0) {
1851+ console.log(`Fuzzy search for ${type} took ${endTime - startTime}ms (cached)`);
1852+ }
18611853 return cache.resultMap.get(searchValue);
18621854 }
18631855
@@ -1872,26 +1864,20 @@ function performFuzzySearch(type, data, keys, searchValue) {
18721864
18731865 const results = fuse.search(searchValue);
18741866 cache.resultMap.set(searchValue, results);
1875- return results;
1867+ let endTime = performance.now();
1876-}
1868+ if (endTime - startTime > 1.0) {
1877-
1869+ console.log(`Fuzzy search for ${type} took ${endTime - startTime}ms`);
1878-
1879-/**
1880- * Clears all fuzzy search caches
1881- */
1882-export function clearFuzzySearchCaches() {
1883- for (const cache of Object.values(fuzzySearchCaches)) {
1884- cache.resultMap.clear();
18851870 }
1886- console.log('Fuzzy search caches cleared');
1871+ return results;
18871872}
18881873
18891874/**
18901875 * Fuzzy search characters by a search term
18911876 * @param {string} searchValue - The search term
1877+ * @param {Object.<string, { resultMap: Map<string, any> }>} fuzzySearchCaches - Fuzzy search caches
18921878 * @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score
18931879 */
18941880export function fuzzySearchCharacters(searchValue, fuzzySearchCaches) {
18951881 const keys = [
18961882 { name: 'data.name', weight: 20 },
18971883 { name: '#tags', weight: 10, getFn: (character) => getTagsList(character.avatar).map(x => x.name).join('||') },
@@ -1906,16 +1892,17 @@ export function fuzzySearchCharacters(searchValue) {
19061892 { name: 'data.alternate_greetings', weight: 1 },
19071893 ];
19081894
19091895 return performFuzzySearch(fuzzySearchCategories.characters, characters, keys, searchValue, fuzzySearchCaches);
19101896}
19111897
19121898/**
19131899 * Fuzzy search world info entries by a search term
19141900 * @param {*[]} data - WI items data array
19151901 * @param {string} searchValue - The search term
1902+ * @param {Object.<string, { resultMap: Map<string, any> }>} fuzzySearchCaches - Fuzzy search caches
19161903 * @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score
19171904 */
19181905export function fuzzySearchWorldInfo(data, searchValue, fuzzySearchCaches) {
19191906 const keys = [
19201907 { name: 'key', weight: 20 },
19211908 { name: 'group', weight: 15 },
@@ -1926,16 +1913,17 @@ export function fuzzySearchWorldInfo(data, searchValue) {
19261913 { name: 'automationId', weight: 1 },
19271914 ];
19281915
19291916 return performFuzzySearch(fuzzySearchCategories.worldInfo, data, keys, searchValue, fuzzySearchCaches);
19301917}
19311918
19321919/**
19331920 * Fuzzy search persona entries by a search term
19341921 * @param {*[]} data - persona data array
19351922 * @param {string} searchValue - The search term
1923+ * @param {Object.<string, { resultMap: Map<string, any> }>} fuzzySearchCaches - Fuzzy search caches
19361924 * @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score
19371925 */
19381926export function fuzzySearchPersonas(data, searchValue, fuzzySearchCaches) {
19391927 const mappedData = data.map(x => ({
19401928 key: x,
19411929 name: power_user.personas[x] ?? '',
@@ -1947,28 +1935,30 @@ export function fuzzySearchPersonas(data, searchValue) {
19471935 { name: 'description', weight: 3 },
19481936 ];
19491937
19501938 return performFuzzySearch(fuzzySearchCategories.personas, mappedData, keys, searchValue, fuzzySearchCaches);
19511939}
19521940
19531941/**
19541942 * Fuzzy search tags by a search term
19551943 * @param {string} searchValue - The search term
1944+ * @param {Object.<string, { resultMap: Map<string, any> }>} fuzzySearchCaches - Fuzzy search caches
19561945 * @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score
19571946 */
19581947export function fuzzySearchTags(searchValue, fuzzySearchCaches) {
19591948 const keys = [
19601949 { name: 'name', weight: 1 },
19611950 ];
19621951
19631952 return performFuzzySearch(fuzzySearchCategories.tags, tags, keys, searchValue, fuzzySearchCaches);
19641953}
19651954
19661955/**
19671956 * Fuzzy search groups by a search term
19681957 * @param {string} searchValue - The search term
1958+ * @param {Object.<string, { resultMap: Map<string, any> }>} fuzzySearchCaches - Fuzzy search caches
19691959 * @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score
19701960 */
19711961export function fuzzySearchGroups(searchValue, fuzzySearchCaches) {
19721962 const keys = [
19731963 { name: 'name', weight: 20 },
19741964 { name: 'members', weight: 15 },
@@ -1976,7 +1966,7 @@ export function fuzzySearchGroups(searchValue) {
19761966 { name: 'id', weight: 1 },
19771967 ];
19781968
19791969 return performFuzzySearch(fuzzySearchCategories.groups, groups, keys, searchValue, fuzzySearchCaches);
19801970}
19811971
19821972/**