Feature: allows sorting tags by most used (#4768) * add sorting tags by most used * Fix whitespaces * Code review updates * Remove commented code * Fix capitalization in comment * Apply review suggestion * Simplify template init * Reformat * Add documentation for appendViewTagToList and printViewTagList functions * Reprint renamed tags regardless of sorting mode --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>

e7af479427601a6affe58ab9bf1373cd5464bdfb

Serena <85121347+AzureKarasuba@users.noreply.github.com>

Signed
5 files changed, +74 -62Ignore whitespace
public/locales/zh-cn.json+4 -0
@@ -2272,6 +2272,10 @@
2272 "Drag handle to reorder. Click name to rename. Click color to change display.": "拖拽左侧三条横线以排序,点击名字以重命名,点击调色盘以切换颜色。",2272 "Drag handle to reorder. Click name to rename. Click color to change display.": "拖拽左侧三条横线以排序,点击名字以重命名,点击调色盘以切换颜色。",
2273 "Click on the folder icon to use this tag as a folder.": "点击文件夹图标来将此标签作为一个文件夹。",2273 "Click on the folder icon to use this tag as a folder.": "点击文件夹图标来将此标签作为一个文件夹。",
2274 "Use alphabetical sorting": "按字母顺序排列",2274 "Use alphabetical sorting": "按字母顺序排列",
2275 "Sort mode": "排序模式",
2276 "Manual (Drag & Drop)": "手动 (拖放)",
2277 "Alphabetical (A-Z)": "按字母 (A-Z)",
2278 "Most Used (By Count)": "按使用次数",
2275 "tags_sorting_desc": "启用后,标签在创建或重命名时会自动按字母顺序排序。\n禁用后,新标签会追加到末尾。\n\n如果通过拖动手动重新排列标签,则自动排序将被禁用。",2279 "tags_sorting_desc": "启用后,标签在创建或重命名时会自动按字母顺序排序。\n禁用后,新标签会追加到末尾。\n\n如果通过拖动手动重新排列标签,则自动排序将被禁用。",
2276 "Are you sure you want to delete the theme?": "你确定要删除这个主题吗?",2280 "Are you sure you want to delete the theme?": "你确定要删除这个主题吗?",
2277 "Hi,": "嗨,",2281 "Hi,": "嗨,",
public/locales/zh-tw.json+4 -0
@@ -2107,6 +2107,10 @@
2107 "Drag handle to reorder. Click name to rename. Click color to change display.": "拖動以重新排序。點選名稱重新命名。點選顏色更改顯示。",2107 "Drag handle to reorder. Click name to rename. Click color to change display.": "拖動以重新排序。點選名稱重新命名。點選顏色更改顯示。",
2108 "Click on the folder icon to use this tag as a folder.": "點選資料夾圖示以將此標籤作為資料夾。",2108 "Click on the folder icon to use this tag as a folder.": "點選資料夾圖示以將此標籤作為資料夾。",
2109 "Use alphabetical sorting": "按字母順序排序 ",2109 "Use alphabetical sorting": "按字母順序排序 ",
2110 "Sort mode": "排序模式",
2111 "Manual (Drag & Drop)": "手動(拖放)",
2112 "Alphabetical (A-Z)": "按字母(A-Z)",
2113 "Most Used (By Count)": "按使用次數",
2110 "tags_sorting_desc": "啟用後,標籤將在建立或重新命名時將自動按字母排序。\n停用時,新標籤將附加到結尾。\n若標籤被手動拖動重新排序,則自動排序將被停用。",2114 "tags_sorting_desc": "啟用後,標籤將在建立或重新命名時將自動按字母排序。\n停用時,新標籤將附加到結尾。\n若標籤被手動拖動重新排序,則自動排序將被停用。",
2111 "and connect to an": "並連線到",2115 "and connect to an": "並連線到",
2112 "You can add more": "您可加入更多",2116 "You can add more": "您可加入更多",
public/scripts/power-user.js+6 -1
@@ -44,7 +44,7 @@ import {
44 updateBindModelTemplatesState,44 updateBindModelTemplatesState,
45} from './instruct-mode.js';45} from './instruct-mode.js';
4646
47import { getTagsList, tag_import_setting, tag_map, tags } from './tags.js';47import { getTagsList, tag_import_setting, tag_map, tag_sort_mode, tags } from './tags.js';
48import { tokenizers } from './tokenizers.js';48import { tokenizers } from './tokenizers.js';
49import { BIAS_CACHE } from './logit-bias.js';49import { BIAS_CACHE } from './logit-bias.js';
50import { renderTemplateAsync } from './templates.js';50import { renderTemplateAsync } from './templates.js';
@@ -216,6 +216,7 @@ export const power_user = {
216 enable_auto_select_input: false,216 enable_auto_select_input: false,
217 enable_md_hotkeys: false,217 enable_md_hotkeys: false,
218 tag_import_setting: tag_import_setting.ASK,218 tag_import_setting: tag_import_setting.ASK,
219 tag_sort_mode: tag_sort_mode.MANUAL,
219 disable_group_trimming: false,220 disable_group_trimming: false,
220 single_line: false,221 single_line: false,
221222
@@ -1556,6 +1557,10 @@ export async function loadPowerUserSettings(settings, data) {
1556 if (settings.power_user.click_to_edit === undefined && settings.power_user.chat_display === chat_styles.DOCUMENT) {1557 if (settings.power_user.click_to_edit === undefined && settings.power_user.chat_display === chat_styles.DOCUMENT) {
1557 settings.power_user.click_to_edit = true;1558 settings.power_user.click_to_edit = true;
1558 }1559 }
1560 if (Object.hasOwn(settings.power_user, 'auto_sort_tags') && !Object.hasOwn(settings.power_user, 'tag_sort_mode')) {
1561 settings.power_user.tag_sort_mode = settings.power_user.auto_sort_tags ? tag_sort_mode.ALPHABETICAL : tag_sort_mode.MANUAL;
1562 delete settings.power_user.auto_sort_tags;
1563 }
1559 Object.assign(power_user, settings.power_user);1564 Object.assign(power_user, settings.power_user);
1560 }1565 }
15611566
public/scripts/tags.js+52 -52
@@ -85,6 +85,13 @@ export const tag_import_setting = {
85 ONLY_EXISTING: 4,85 ONLY_EXISTING: 4,
86};86};
8787
88/** @enum {string} */
89export const tag_sort_mode = {
90 MANUAL: 'manual',
91 ALPHABETICAL: 'alphabetical',
92 BY_ENTRIES: 'by_entries',
93};
94
88/**95/**
89 * @type {{ FAV: Tag, GROUP: Tag, FOLDER: Tag, VIEW: Tag, HINT: Tag, UNFILTER: Tag }}96 * @type {{ FAV: Tag, GROUP: Tag, FOLDER: Tag, VIEW: Tag, HINT: Tag, UNFILTER: Tag }}
90 * A collection of global actional tags for the filter panel97 * A collection of global actional tags for the filter panel
@@ -140,6 +147,7 @@ const TAG_FOLDER_DEFAULT_TYPE = 'NONE';
140 * @property {string} [folder_type] - The bogus folder type of this tag (based on `TAG_FOLDER_TYPES`)147 * @property {string} [folder_type] - The bogus folder type of this tag (based on `TAG_FOLDER_TYPES`)
141 * @property {string} [filter_state] - The saved state of the filter chosen of this tag (based on `FILTER_STATES`)148 * @property {string} [filter_state] - The saved state of the filter chosen of this tag (based on `FILTER_STATES`)
142 * @property {number} [sort_order] - A custom integer representing the sort order if tags are sorted149 * @property {number} [sort_order] - A custom integer representing the sort order if tags are sorted
150 * @property {number} [count] - The number of entities that have this tag assigned
143 * @property {string} [color] - The background color of the tag151 * @property {string} [color] - The background color of the tag
144 * @property {string} [color2] - The foreground color of the tag152 * @property {string} [color2] - The foreground color of the tag
145 * @property {number} [create_date] - A number representing the date when this tag was created153 * @property {number} [create_date] - A number representing the date when this tag was created
@@ -1331,44 +1339,26 @@ export function createTagInput(inputSelector, listSelector, tagListOptions = {})
1331async function onViewTagsListClick() {1339async function onViewTagsListClick() {
1332 const html = $(document.createElement('div'));1340 const html = $(document.createElement('div'));
1333 html.attr('id', 'tag_view_list');1341 html.attr('id', 'tag_view_list');
1334 html.append(await renderTemplateAsync('tagManagement', { bogus_folders: power_user.bogus_folders, auto_sort_tags: power_user.auto_sort_tags }));1342 html.append(await renderTemplateAsync('tagManagement', { bogus_folders: power_user.bogus_folders }));
13351343
1336 const tagContainer = $('<div class="tag_view_list_tags ui-sortable"></div>');1344 const tagContainer = $('<div class="tag_view_list_tags ui-sortable"></div>');
1337 html.append(tagContainer);1345 html.append(tagContainer);
13381346
1347 const $sortModeSelect = html.find('#tag_sort_mode_select');
1348 $sortModeSelect.val(power_user.tag_sort_mode);
1349 $sortModeSelect.on('change', function () {
1350 const newMode = $(this).val().toString();
1351 power_user.tag_sort_mode = newMode;
1352 saveSettingsDebounced();
1353 printViewTagList(tagContainer);
1354 });
1355
1339 printViewTagList(tagContainer);1356 printViewTagList(tagContainer);
1340 makeTagListDraggable(tagContainer);1357 makeTagListDraggable(tagContainer);
13411358
1342 await callGenericPopup(html, POPUP_TYPE.TEXT, null, { allowVerticalScrolling: true, wide: true, large: true });1359 await callGenericPopup(html, POPUP_TYPE.TEXT, null, { allowVerticalScrolling: true, wide: true, large: true });
1343}1360}
13441361
1345/**
1346 * Print the list of tags in the tag management view
1347 * @param {Event} event Event that triggered the color change
1348 * @param {boolean} toggle State of the toggle
1349 */
1350function toggleAutoSortTags(event, toggle) {
1351 if (toggle === power_user.auto_sort_tags) return;
1352
1353 // Ask user to confirm if enabling and it was manually sorted before
1354 if (toggle && isManuallySorted() && !confirm('Are you sure you want to automatically sort alphabetically?')) {
1355 if (event.target instanceof HTMLInputElement) {
1356 event.target.checked = false;
1357 }
1358 return;
1359 }
1360
1361 power_user.auto_sort_tags = toggle;
1362
1363 printCharactersDebounced();
1364 saveSettingsDebounced();
1365}
1366
1367/** This function goes over all existing tags and checks whether they were reorderd in the past. @returns {boolean} */
1368function isManuallySorted() {
1369 return tags.some((tag, index) => tag.sort_order !== index);
1370}
1371
1372function makeTagListDraggable(tagContainer) {1362function makeTagListDraggable(tagContainer) {
1373 const onTagsSort = () => {1363 const onTagsSort = () => {
1374 tagContainer.find('.tag_view_item').each(function (i, tagElement) {1364 tagContainer.find('.tag_view_item').each(function (i, tagElement) {
@@ -1380,10 +1370,10 @@ function makeTagListDraggable(tagContainer) {
1380 });1370 });
13811371
1382 // If tags were dragged manually, we have to disable auto sorting1372 // If tags were dragged manually, we have to disable auto sorting
1383 if (power_user.auto_sort_tags) {1373 if (power_user.tag_sort_mode !== tag_sort_mode.MANUAL) {
1384 power_user.auto_sort_tags = false;1374 power_user.tag_sort_mode = tag_sort_mode.MANUAL;
1385 $('#tag_view_list input[name="auto_sort_tags"]').prop('checked', false);1375 $('#tag_sort_mode_select').val(tag_sort_mode.MANUAL);
1386 toastr.info('Automatic sorting of tags deactivated.');1376 toastr.info('Switched to Manual sorting mode.');
1387 }1377 }
13881378
1389 // If the order of tags in display has changed, we need to redraw some UI elements. Do it debounced so it doesn't block and you can drag multiple tags.1379 // If the order of tags in display has changed, we need to redraw some UI elements. Do it debounced so it doesn't block and you can drag multiple tags.
@@ -1417,11 +1407,20 @@ function sortTags(tags) {
1417 * @returns {number} The compare result1407 * @returns {number} The compare result
1418 */1408 */
1419function compareTagsForSort(a, b) {1409function compareTagsForSort(a, b) {
1410 // default sort: alphabetical, case insensitive
1420 const defaultSort = a.name.toLowerCase().localeCompare(b.name.toLowerCase());1411 const defaultSort = a.name.toLowerCase().localeCompare(b.name.toLowerCase());
1421 if (power_user.auto_sort_tags) {1412
1413 // sort on number of entries
1414 if (power_user.tag_sort_mode === tag_sort_mode.BY_ENTRIES) {
1415 return ((b.count || 0) - (a.count || 0)) || defaultSort;
1416 }
1417
1418 // alphabetical sort
1419 if (power_user.tag_sort_mode === tag_sort_mode.ALPHABETICAL) {
1422 return defaultSort;1420 return defaultSort;
1423 }1421 }
14241422
1423 // manual sort
1425 if (a.sort_order !== undefined && b.sort_order !== undefined) {1424 if (a.sort_order !== undefined && b.sort_order !== undefined) {
1426 return a.sort_order - b.sort_order;1425 return a.sort_order - b.sort_order;
1427 } else if (a.sort_order !== undefined) {1426 } else if (a.sort_order !== undefined) {
@@ -1620,8 +1619,13 @@ function onTagCreateClick() {
1620 toastr.success('Tag created', 'Create Tag');1619 toastr.success('Tag created', 'Create Tag');
1621}1620}
16221621
1623function appendViewTagToList(list, tag, everything) {1622/**
1624 const count = everything.filter(x => x == tag.id).length;1623 * Appends a tag to the view tag list.
1624 * @param {JQuery<HTMLElement>} list List element
1625 * @param {Tag} tag Tag object
1626 * @param {number} count Count of characters/groups using this tag
1627 */
1628function appendViewTagToList(list, tag, count) {
1625 const template = VIEW_TAG_TEMPLATE.clone();1629 const template = VIEW_TAG_TEMPLATE.clone();
1626 template.attr('id', tag.id);1630 template.attr('id', tag.id);
1627 template.find('.tag_view_counter_value').text(count);1631 template.find('.tag_view_counter_value').text(count);
@@ -1869,12 +1873,21 @@ function copyTags(data) {
1869 tag_map[data.newAvatar] = Array.from(new Set([...prevTagMap, ...newTagMap]));1873 tag_map[data.newAvatar] = Array.from(new Set([...prevTagMap, ...newTagMap]));
1870}1874}
18711875
1876/**
1877 * Prints the tag list in the view tags popup.
1878 * @param {JQuery<HTMLElement>} tagContainer Container element
1879 * @param {boolean} empty Whether to empty the container before printing
1880 */
1872function printViewTagList(tagContainer, empty = true) {1881function printViewTagList(tagContainer, empty = true) {
1873 if (empty) tagContainer.empty();1882 if (empty) tagContainer.empty();
1874 const everything = Object.values(tag_map).flat();1883 const everything = Object.values(tag_map).flat();
1875 const sortedTags = sortTags(tags);1884 const tagsWithCounts = tags.map(tag => {
1885 const count = everything.filter(x => x === tag.id).length;
1886 return { ...tag, count: count };
1887 });
1888 const sortedTags = sortTags(tagsWithCounts);
1876 for (const tag of sortedTags) {1889 for (const tag of sortedTags) {
1877 appendViewTagToList(tagContainer, tag, everything);1890 appendViewTagToList(tagContainer, tag, tag.count);
1878 }1891 }
1879}1892}
18801893
@@ -2296,14 +2309,9 @@ export function initTags() {
2296 eventSource.on(event_types.CHARACTER_DUPLICATED, copyTags);2309 eventSource.on(event_types.CHARACTER_DUPLICATED, copyTags);
2297 eventSource.makeFirst(event_types.CHAT_CHANGED, () => selected_group ? applyTagsOnGroupSelect() : applyTagsOnCharacterSelect());2310 eventSource.makeFirst(event_types.CHAT_CHANGED, () => selected_group ? applyTagsOnGroupSelect() : applyTagsOnCharacterSelect());
22982311
2299 $(document).on('input', '#tag_view_list input[name="auto_sort_tags"]', (evt) => {
2300 const toggle = $(evt.target).is(':checked');
2301 toggleAutoSortTags(evt.originalEvent, toggle);
2302 printViewTagList($('#tag_view_list .tag_view_list_tags'));
2303 });
2304 $(document).on('focusout', '#tag_view_list .tag_view_name', (evt) => {2312 $(document).on('focusout', '#tag_view_list .tag_view_name', (evt) => {
2305 // Reorder/reprint tags, but only if the name actually has changed, and only if we auto sort tags2313 // Reorder/reprint tags, but only if the name actually has changed
2306 if (!power_user.auto_sort_tags || !$(evt.target).is('[dirty]')) return;2314 if (!$(evt.target).is('[dirty]')) return;
23072315
2308 // Remember the order, so we can flash highlight if it changed after reprinting2316 // Remember the order, so we can flash highlight if it changed after reprinting
2309 const tagId = ($(evt.target).closest('.tag_view_item')).attr('id');2317 const tagId = ($(evt.target).closest('.tag_view_item')).attr('id');
@@ -2324,14 +2332,6 @@ export function initTags() {
2324 }2332 }
2325 });2333 });
23262334
2327 // Initialize auto sort setting based on whether it was sorted before
2328 if (power_user.auto_sort_tags === undefined || power_user.auto_sort_tags === null) {
2329 power_user.auto_sort_tags = !isManuallySorted();
2330 if (power_user.auto_sort_tags) {
2331 printCharactersDebounced();
2332 }
2333 }
2334
2335 registerTagsSlashCommands();2335 registerTagsSlashCommands();
2336 restoreSavedTagFilters();2336 restoreSavedTagFilters();
2337}2337}
public/scripts/templates/tagManagement.html+8 -9
@@ -21,17 +21,16 @@
21 </div>21 </div>
22 </div>22 </div>
23 <div class="justifyLeft m-b-1">23 <div class="justifyLeft m-b-1">
24 <div class="flex-container alignItemsBaseline">
25 <span data-i18n="Sort mode">Sort mode:</span>
26 <select id="tag_sort_mode_select" class="flex1 text_pole">
27 <option value="manual" data-i18n="Manual (Drag & Drop)">Manual (Drag & Drop)</option>
28 <option value="alphabetical" data-i18n="Alphabetical (A-Z)">Alphabetical (A-Z)</option>
29 <option value="by_entries" data-i18n="Most Used (By Count)">Most Used (By Count)</option>
30 </select>
31 </div>
24 <small>32 <small>
25 <span data-i18n="Drag handle to reorder. Click name to rename. Click color to change display.">Drag handle to reorder. Click name to rename. Click color to change display.</span><br>33 <span data-i18n="Drag handle to reorder. Click name to rename. Click color to change display.">Drag handle to reorder. Click name to rename. Click color to change display.</span><br>
26 {{#if bogus_folders}}<span data-i18n="Click on the folder icon to use this tag as a folder.">Click on the folder icon to use this tag as a folder.</span><br>{{/if}}34 {{#if bogus_folders}}<span data-i18n="Click on the folder icon to use this tag as a folder.">Click on the folder icon to use this tag as a folder.</span><br>{{/if}}
27 <label class="checkbox_label flex-container alignItemsCenter m-t-1" for="auto_sort_tags">
28 <input type="checkbox" id="auto_sort_tags" name="auto_sort_tags" {{#if auto_sort_tags}} checked{{/if}} />
29 <span data-i18n="Use alphabetical sorting">
30 Use alphabetical sorting
31 </span>
32 <div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]tags_sorting_desc"
33 title="If enabled, tags will automatically be sorted alphabetically on creation or rename.&#10;If disabled, new tags will be appended at the end.&#10;&#10;If a tag is manually reordered by dragging, automatic sorting will be disabled.">
34 </div>
35 </label>
36 </small>35 </small>
37 </div>36 </div>