Add shallow character load mode
| @@ -4424,6 +4424,13 @@ | ||
| 4424 | 4424 | <option data-i18n="tag_import_existing" value="4">Existing</option> |
| 4425 | 4425 | </select> |
| 4426 | 4426 | </div> |
| 4427 | + <label class="checkbox_label" for="shallow_characters" data-i18n="[title]Experimental feature. May behave unstable and have compatibility issues." title="Experimental feature. May behave unstable and have compatibility issues."> | |
| 4428 | + <input id="shallow_characters" type="checkbox" /> | |
| 4429 | + <small data-i18n="Lazy Load Characters"> | |
| 4430 | + Lazy Load Characters | |
| 4431 | + </small> | |
| 4432 | + <i class="fa-solid fa-flask"></i> | |
| 4433 | + </label> | |
| 4427 | 4434 | <label class="checkbox_label" for="fuzzy_search_checkbox" title="Use fuzzy matching, and search characters in the list by all data fields, not just by a name substring." data-i18n="[title]Use fuzzy matching, and search characters in the list by all data fields, not just by a name substring"> |
| 4428 | 4435 | <input id="fuzzy_search_checkbox" type="checkbox" /> |
| 4429 | 4436 | <small data-i18n="Advanced Character Search">Advanced Character Search</small> |
| @@ -1781,7 +1781,7 @@ export async function getCharacters() { | ||
| 1781 | 1781 | method: 'POST', |
| 1782 | 1782 | headers: getRequestHeaders(), |
| 1783 | 1783 | body: JSON.stringify({ |
| 1784 | 1784 | ''shallow: ''power_user.shallow_characters, |
| 1785 | 1785 | }), |
| 1786 | 1786 | }); |
| 1787 | 1787 | if (response.ok === true) { |
| @@ -3658,6 +3658,9 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | ||
| 3658 | 3658 | setGenerationProgress(0); |
| 3659 | 3659 | generation_started = new Date(); |
| 3660 | 3660 | |
| 3661 | + // Prevent generation from shallow characters | |
| 3662 | + await unshallowCharacter(this_chid); | |
| 3663 | + | |
| 3661 | 3664 | // Occurs every time, even if the generation is aborted due to slash commands execution |
| 3662 | 3665 | await eventSource.emit(event_types.GENERATION_STARTED, type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage }, dryRun); |
| 3663 | 3666 | |
| @@ -6704,9 +6707,42 @@ export function buildAvatarList(block, entities, { templateId = 'inline_avatar_t | ||
| 6704 | 6707 | } |
| 6705 | 6708 | } |
| 6706 | 6709 | |
| 6710 | +/** | |
| 6711 | + * Loads all the data of a shallow character. | |
| 6712 | + * @param {string|undefined} characterId Array index | |
| 6713 | + * @returns {Promise<void>} Promise that resolves when the character is unshallowed | |
| 6714 | + */ | |
| 6715 | +export async function unshallowCharacter(characterId) { | |
| 6716 | + if (characterId === undefined) { | |
| 6717 | + console.warn('Undefined character cannot be unshallowed'); | |
| 6718 | + return; | |
| 6719 | + } | |
| 6720 | + | |
| 6721 | + const character = characters[characterId]; | |
| 6722 | + if (!character) { | |
| 6723 | + console.warn('Character not found:', characterId); | |
| 6724 | + return; | |
| 6725 | + } | |
| 6726 | + | |
| 6727 | + // Character is not shallow | |
| 6728 | + if (!character.shallow) { | |
| 6729 | + return; | |
| 6730 | + } | |
| 6731 | + | |
| 6732 | + const avatar = character.avatar; | |
| 6733 | + if (!avatar) { | |
| 6734 | + console.warn('Character has no avatar field:', characterId); | |
| 6735 | + return; | |
| 6736 | + } | |
| 6737 | + | |
| 6738 | + await getOneCharacter(avatar); | |
| 6739 | +} | |
| 6740 | + | |
| 6707 | 6741 | export async function getChat() { |
| 6708 | 6742 | //console.log('/api/chats/get -- entered for -- ' + characters[this_chid].name); |
| 6709 | 6743 | try { |
| 6744 | + await unshallowCharacter(this_chid); | |
| 6745 | + | |
| 6710 | 6746 | const response = await $.ajax({ |
| 6711 | 6747 | type: 'POST', |
| 6712 | 6748 | url: '/api/chats/get', |
| @@ -72,6 +72,7 @@ import { | ||
| 72 | 72 | animation_duration, |
| 73 | 73 | depth_prompt_role_default, |
| 74 | 74 | shouldAutoContinue, |
| 75 | + unshallowCharacter, | |
| 75 | 76 | } from '../script.js'; |
| 76 | 77 | import { printTagList, createTagMapFromList, applyTagsOnCharacterSelect, tag_map, applyTagsOnGroupSelect } from './tags.js'; |
| 77 | 78 | import { FILTER_TYPES, FilterHelper } from './filters.js'; |
| @@ -216,6 +217,7 @@ export async function getGroupChat(groupId, reload = false) { | ||
| 216 | 217 | |
| 217 | 218 | // Run validation before any loading |
| 218 | 219 | validateGroup(group); |
| 220 | + await unshallowGroupMembers(groupId); | |
| 219 | 221 | |
| 220 | 222 | const chat_id = group.chat_id; |
| 221 | 223 | const data = await loadGroupChat(chat_id); |
| @@ -824,6 +826,8 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) { | ||
| 824 | 826 | } |
| 825 | 827 | |
| 826 | 828 | try { |
| 829 | + await unshallowGroupMembers(selected_group); | |
| 830 | + | |
| 827 | 831 | throwIfAborted(); |
| 828 | 832 | hideSwipeButtons(); |
| 829 | 833 | is_group_generating = true; |
| @@ -1137,6 +1141,29 @@ export async function editGroup(id, immediately, reload = true) { | ||
| 1137 | 1141 | saveGroupDebounced(group, reload); |
| 1138 | 1142 | } |
| 1139 | 1143 | |
| 1144 | +/** | |
| 1145 | + * Unshallows all definitions of group members. | |
| 1146 | + * @param {string} groupId Id of the group | |
| 1147 | + * @returns {Promise<void>} Promise that resolves when all group members are unshallowed | |
| 1148 | + */ | |
| 1149 | +export async function unshallowGroupMembers(groupId) { | |
| 1150 | + const group = groups.find(x => x.id == groupId); | |
| 1151 | + if (!group) { | |
| 1152 | + return; | |
| 1153 | + } | |
| 1154 | + const members = group.members; | |
| 1155 | + if (!Array.isArray(members)) { | |
| 1156 | + return; | |
| 1157 | + } | |
| 1158 | + for (const member of members) { | |
| 1159 | + const index = characters.findIndex(x => x.avatar === member); | |
| 1160 | + if (index === -1) { | |
| 1161 | + continue; | |
| 1162 | + } | |
| 1163 | + await unshallowCharacter(String(index)); | |
| 1164 | + } | |
| 1165 | +} | |
| 1166 | + | |
| 1140 | 1167 | let groupAutoModeAbortController = null; |
| 1141 | 1168 | |
| 1142 | 1169 | async function groupChatAutoModeWorker() { |
| @@ -1158,9 +1185,9 @@ async function groupChatAutoModeWorker() { | ||
| 1158 | 1185 | await generateGroupWrapper(true, 'auto', { signal: groupAutoModeAbortController.signal }); |
| 1159 | 1186 | } |
| 1160 | 1187 | |
| 1161 | 1188 | async function modifyGroupMember(chat_idgroupId, groupMember, isDelete) { |
| 1162 | 1189 | const id = groupMember.data('id'); |
| 1163 | 1190 | const thisGroup = groups.find((x) => x.id == chat_idgroupId); |
| 1164 | 1191 | const membersArray = thisGroup?.members ?? newGroupMembers; |
| 1165 | 1192 | |
| 1166 | 1193 | if (isDelete) { |
| @@ -1173,6 +1200,7 @@ async function modifyGroupMember(chat_id, groupMember, isDelete) { | ||
| 1173 | 1200 | } |
| 1174 | 1201 | |
| 1175 | 1202 | if (openGroupId) { |
| 1203 | + await unshallowGroupMembers(openGroupId); | |
| 1176 | 1204 | await editGroup(openGroupId, false, false); |
| 1177 | 1205 | updateGroupAvatar(thisGroup); |
| 1178 | 1206 | } |
| @@ -1638,7 +1666,7 @@ async function onGroupActionClick(event) { | ||
| 1638 | 1666 | } |
| 1639 | 1667 | |
| 1640 | 1668 | if (action === 'view') { |
| 1641 | 1669 | await openCharacterDefinition(member); |
| 1642 | 1670 | } |
| 1643 | 1671 | |
| 1644 | 1672 | if (action === 'speak') { |
| @@ -1690,7 +1718,7 @@ export async function openGroupById(groupId) { | ||
| 1690 | 1718 | return false; |
| 1691 | 1719 | } |
| 1692 | 1720 | |
| 1693 | 1721 | async function openCharacterDefinition(characterSelect) { |
| 1694 | 1722 | if (is_group_generating) { |
| 1695 | 1723 | toastr.warning(t`Can't peek a character while group reply is being generated`); |
| 1696 | 1724 | console.warn('Can\'t peek a character def while group reply is being generated'); |
| @@ -1703,6 +1731,7 @@ function openCharacterDefinition(characterSelect) { | ||
| 1703 | 1731 | return; |
| 1704 | 1732 | } |
| 1705 | 1733 | |
| 1734 | + await unshallowCharacter(chid); | |
| 1706 | 1735 | setCharacterId(chid); |
| 1707 | 1736 | select_selected_character(chid); |
| 1708 | 1737 | // Gentle nudge to recalculate tokens |
| @@ -314,6 +314,7 @@ let power_user = { | ||
| 314 | 314 | forbid_external_media: true, |
| 315 | 315 | external_media_allowed_overrides: [], |
| 316 | 316 | external_media_forbidden_overrides: [], |
| 317 | + shallow_characters: false, | |
| 317 | 318 | }; |
| 318 | 319 | |
| 319 | 320 | let themes = []; |
| @@ -1595,6 +1596,7 @@ async function loadPowerUserSettings(settings, data) { | ||
| 1595 | 1596 | $('#auto-connect-checkbox').prop('checked', power_user.auto_connect); |
| 1596 | 1597 | $('#auto-load-chat-checkbox').prop('checked', power_user.auto_load_chat); |
| 1597 | 1598 | $('#forbid_external_media').prop('checked', power_user.forbid_external_media); |
| 1599 | + $('#shallow_characters').prop('checked', power_user.shallow_characters); | |
| 1598 | 1600 | |
| 1599 | 1601 | for (const theme of themes) { |
| 1600 | 1602 | const option = document.createElement('option'); |
| @@ -3888,6 +3890,12 @@ $(document).ready(() => { | ||
| 3888 | 3890 | await exportTheme(); |
| 3889 | 3891 | }); |
| 3890 | 3892 | |
| 3893 | + $('#shallow_characters').on('input', function () { | |
| 3894 | + power_user.shallow_characters = !!$(this).prop('checked'); | |
| 3895 | + saveSettingsDebounced(); | |
| 3896 | + toastr.info('Reload the page for this setting to take effect'); | |
| 3897 | + }); | |
| 3898 | + | |
| 3891 | 3899 | $(document).on('click', '#debug_table [data-debug-function]', function () { |
| 3892 | 3900 | const functionId = $(this).data('debug-function'); |
| 3893 | 3901 | const functionRecord = debug_functions.find(f => f.functionId === functionId); |
| @@ -47,6 +47,7 @@ import { | ||
| 47 | 47 | updateMessageBlock, |
| 48 | 48 | printMessages, |
| 49 | 49 | clearChat, |
| 50 | + unshallowCharacter, | |
| 50 | 51 | } from '../script.js'; |
| 51 | 52 | import { |
| 52 | 53 | extension_settings, |
| @@ -55,7 +56,7 @@ import { | ||
| 55 | 56 | renderExtensionTemplateAsync, |
| 56 | 57 | writeExtensionField, |
| 57 | 58 | } from './extensions.js'; |
| 58 | 59 | import { groups, openGroupChat, selected_group, unshallowGroupMembers } from './group-chats.js'; |
| 59 | 60 | import { addLocaleData, getCurrentLocale, t, translate } from './i18n.js'; |
| 60 | 61 | import { hideLoader, showLoader } from './loader.js'; |
| 61 | 62 | import { MacrosParser } from './macros.js'; |
| @@ -210,6 +211,8 @@ export function getContext() { | ||
| 210 | 211 | clearChat, |
| 211 | 212 | ChatCompletionService, |
| 212 | 213 | TextCompletionService, |
| 214 | + unshallowCharacter, | |
| 215 | + unshallowGroupMembers, | |
| 213 | 216 | }; |
| 214 | 217 | } |
| 215 | 218 | |
| @@ -201,13 +201,44 @@ const calculateDataSize = (data) => { | ||
| 201 | 201 | }; |
| 202 | 202 | |
| 203 | 203 | /** |
| 204 | + * Only get fields that are used to display the character list. | |
| 205 | + * @param {object} character Character object | |
| 206 | + * @returns {{shallow: true, [key: string]: any}} Shallow character | |
| 207 | + */ | |
| 208 | +const toShallow = (character) => { | |
| 209 | + return { | |
| 210 | + name: character.name, | |
| 211 | + avatar: character.avatar, | |
| 212 | + chat: character.chat, | |
| 213 | + fav: character.fav, | |
| 214 | + date_added: character.date_added, | |
| 215 | + create_date: character.create_date, | |
| 216 | + date_last_chat: character.date_last_chat, | |
| 217 | + chat_size: character.chat_size, | |
| 218 | + data_size: character.data_size, | |
| 219 | + data: { | |
| 220 | + name: _.get(character, 'data.name', ''), | |
| 221 | + character_version: _.get(character, 'data.character_version', ''), | |
| 222 | + creator: _.get(character, 'data.creator', ''), | |
| 223 | + creator_notes: _.get(character, 'data.creator_notes', ''), | |
| 224 | + extensions: { | |
| 225 | + fav: _.get(character, 'data.extensions.fav', false), | |
| 226 | + }, | |
| 227 | + }, | |
| 228 | + shallow: true, | |
| 229 | + }; | |
| 230 | +}; | |
| 231 | + | |
| 232 | +/** | |
| 204 | 233 | * processCharacter - Process a given character, read its data and calculate its statistics. |
| 205 | 234 | * |
| 206 | 235 | * @param {string} item The name of the character. |
| 207 | 236 | * @param {import('../users.js').UserDirectoryList} directories User directories |
| 237 | + * @param {object} options Options for the character processing | |
| 238 | + * @param {boolean} options.shallow If true, only return the core character's metadata | |
| 208 | 239 | * @return {Promise<object>} A Promise that resolves when the character processing is done. |
| 209 | 240 | */ |
| 210 | 241 | const processCharacter = async (item, directories, { shallow }) => { |
| 211 | 242 | try { |
| 212 | 243 | const imgFile = path.join(directories.characters, item); |
| 213 | 244 | const imgData = await readCharacterData(imgFile); |
| @@ -226,7 +257,7 @@ const processCharacter = async (item, directories) => { | ||
| 226 | 257 | character['chat_size'] = chatSize; |
| 227 | 258 | character['date_last_chat'] = dateLastChat; |
| 228 | 259 | character['data_size'] = calculateDataSize(jsonObject?.data); |
| 229 | - return character; | |
| 260 | + return shallow ? toShallow(character) : character; | |
| 230 | 261 | } |
| 231 | 262 | catch (err) { |
| 232 | 263 | console.error(`Could not process character: ${item}`); |
| @@ -991,9 +1022,10 @@ router.post('/delete', jsonParser, validateAvatarUrlMiddleware, async function ( | ||
| 991 | 1022 | */ |
| 992 | 1023 | router.post('/all', jsonParser, async function (request, response) { |
| 993 | 1024 | try { |
| 1025 | + const shallow = !!request.body.shallow; | |
| 994 | 1026 | const files = fs.readdirSync(request.user.directories.characters); |
| 995 | 1027 | const pngFiles = files.filter(file => file.endsWith('.png')); |
| 996 | 1028 | const processingPromises = pngFiles.map(file => processCharacter(file, request.user.directories, { shallow })); |
| 997 | 1029 | const data = (await Promise.all(processingPromises)).filter(c => c.name); |
| 998 | 1030 | return response.send(data); |
| 999 | 1031 | } catch (err) { |
| @@ -1012,7 +1044,7 @@ router.post('/get', jsonParser, validateAvatarUrlMiddleware, async function (req | ||
| 1012 | 1044 | return response.sendStatus(404); |
| 1013 | 1045 | } |
| 1014 | 1046 | |
| 1015 | 1047 | const data = await processCharacter(item, request.user.directories, { shallow: false }); |
| 1016 | 1048 | |
| 1017 | 1049 | return response.send(data); |
| 1018 | 1050 | } catch (err) { |