Add shallow character load mode
| @@ -4424,6 +4424,13 @@ | |||
| 4424 | <option data-i18n="tag_import_existing" value="4">Existing</option> | 4424 | <option data-i18n="tag_import_existing" value="4">Existing</option> |
| 4425 | </select> | 4425 | </select> |
| 4426 | </div> | 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 | <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"> | 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 | <input id="fuzzy_search_checkbox" type="checkbox" /> | 4435 | <input id="fuzzy_search_checkbox" type="checkbox" /> |
| 4429 | <small data-i18n="Advanced Character Search">Advanced Character Search</small> | 4436 | <small data-i18n="Advanced Character Search">Advanced Character Search</small> |
| @@ -1781,7 +1781,7 @@ export async function getCharacters() { | |||
| 1781 | method: 'POST', | 1781 | method: 'POST', |
| 1782 | headers: getRequestHeaders(), | 1782 | headers: getRequestHeaders(), |
| 1783 | body: JSON.stringify({ | 1783 | body: JSON.stringify({ |
| 1784 | '': '', | 1784 | shallow: power_user.shallow_characters, |
| 1785 | }), | 1785 | }), |
| 1786 | }); | 1786 | }); |
| 1787 | if (response.ok === true) { | 1787 | if (response.ok === true) { |
| @@ -3658,6 +3658,9 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | |||
| 3658 | setGenerationProgress(0); | 3658 | setGenerationProgress(0); |
| 3659 | generation_started = new Date(); | 3659 | generation_started = new Date(); |
| 3660 | 3660 | ||
| 3661 | // Prevent generation from shallow characters | ||
| 3662 | await unshallowCharacter(this_chid); | ||
| 3663 | |||
| 3661 | // Occurs every time, even if the generation is aborted due to slash commands execution | 3664 | // Occurs every time, even if the generation is aborted due to slash commands execution |
| 3662 | await eventSource.emit(event_types.GENERATION_STARTED, type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage }, dryRun); | 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 | export async function getChat() { | 6741 | export async function getChat() { |
| 6708 | //console.log('/api/chats/get -- entered for -- ' + characters[this_chid].name); | 6742 | //console.log('/api/chats/get -- entered for -- ' + characters[this_chid].name); |
| 6709 | try { | 6743 | try { |
| 6744 | await unshallowCharacter(this_chid); | ||
| 6745 | |||
| 6710 | const response = await $.ajax({ | 6746 | const response = await $.ajax({ |
| 6711 | type: 'POST', | 6747 | type: 'POST', |
| 6712 | url: '/api/chats/get', | 6748 | url: '/api/chats/get', |
| @@ -72,6 +72,7 @@ import { | |||
| 72 | animation_duration, | 72 | animation_duration, |
| 73 | depth_prompt_role_default, | 73 | depth_prompt_role_default, |
| 74 | shouldAutoContinue, | 74 | shouldAutoContinue, |
| 75 | unshallowCharacter, | ||
| 75 | } from '../script.js'; | 76 | } from '../script.js'; |
| 76 | import { printTagList, createTagMapFromList, applyTagsOnCharacterSelect, tag_map, applyTagsOnGroupSelect } from './tags.js'; | 77 | import { printTagList, createTagMapFromList, applyTagsOnCharacterSelect, tag_map, applyTagsOnGroupSelect } from './tags.js'; |
| 77 | import { FILTER_TYPES, FilterHelper } from './filters.js'; | 78 | import { FILTER_TYPES, FilterHelper } from './filters.js'; |
| @@ -216,6 +217,7 @@ export async function getGroupChat(groupId, reload = false) { | |||
| 216 | 217 | ||
| 217 | // Run validation before any loading | 218 | // Run validation before any loading |
| 218 | validateGroup(group); | 219 | validateGroup(group); |
| 220 | await unshallowGroupMembers(groupId); | ||
| 219 | 221 | ||
| 220 | const chat_id = group.chat_id; | 222 | const chat_id = group.chat_id; |
| 221 | const data = await loadGroupChat(chat_id); | 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 | try { | 828 | try { |
| 829 | await unshallowGroupMembers(selected_group); | ||
| 830 | |||
| 827 | throwIfAborted(); | 831 | throwIfAborted(); |
| 828 | hideSwipeButtons(); | 832 | hideSwipeButtons(); |
| 829 | is_group_generating = true; | 833 | is_group_generating = true; |
| @@ -1137,6 +1141,29 @@ export async function editGroup(id, immediately, reload = true) { | |||
| 1137 | saveGroupDebounced(group, reload); | 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 | let groupAutoModeAbortController = null; | 1167 | let groupAutoModeAbortController = null; |
| 1141 | 1168 | ||
| 1142 | async function groupChatAutoModeWorker() { | 1169 | async function groupChatAutoModeWorker() { |
| @@ -1158,9 +1185,9 @@ async function groupChatAutoModeWorker() { | |||
| 1158 | await generateGroupWrapper(true, 'auto', { signal: groupAutoModeAbortController.signal }); | 1185 | await generateGroupWrapper(true, 'auto', { signal: groupAutoModeAbortController.signal }); |
| 1159 | } | 1186 | } |
| 1160 | 1187 | ||
| 1161 | async function modifyGroupMember(chat_id, groupMember, isDelete) { | 1188 | async function modifyGroupMember(groupId, groupMember, isDelete) { |
| 1162 | const id = groupMember.data('id'); | 1189 | const id = groupMember.data('id'); |
| 1163 | const thisGroup = groups.find((x) => x.id == chat_id); | 1190 | const thisGroup = groups.find((x) => x.id == groupId); |
| 1164 | const membersArray = thisGroup?.members ?? newGroupMembers; | 1191 | const membersArray = thisGroup?.members ?? newGroupMembers; |
| 1165 | 1192 | ||
| 1166 | if (isDelete) { | 1193 | if (isDelete) { |
| @@ -1173,6 +1200,7 @@ async function modifyGroupMember(chat_id, groupMember, isDelete) { | |||
| 1173 | } | 1200 | } |
| 1174 | 1201 | ||
| 1175 | if (openGroupId) { | 1202 | if (openGroupId) { |
| 1203 | await unshallowGroupMembers(openGroupId); | ||
| 1176 | await editGroup(openGroupId, false, false); | 1204 | await editGroup(openGroupId, false, false); |
| 1177 | updateGroupAvatar(thisGroup); | 1205 | updateGroupAvatar(thisGroup); |
| 1178 | } | 1206 | } |
| @@ -1638,7 +1666,7 @@ async function onGroupActionClick(event) { | |||
| 1638 | } | 1666 | } |
| 1639 | 1667 | ||
| 1640 | if (action === 'view') { | 1668 | if (action === 'view') { |
| 1641 | openCharacterDefinition(member); | 1669 | await openCharacterDefinition(member); |
| 1642 | } | 1670 | } |
| 1643 | 1671 | ||
| 1644 | if (action === 'speak') { | 1672 | if (action === 'speak') { |
| @@ -1690,7 +1718,7 @@ export async function openGroupById(groupId) { | |||
| 1690 | return false; | 1718 | return false; |
| 1691 | } | 1719 | } |
| 1692 | 1720 | ||
| 1693 | function openCharacterDefinition(characterSelect) { | 1721 | async function openCharacterDefinition(characterSelect) { |
| 1694 | if (is_group_generating) { | 1722 | if (is_group_generating) { |
| 1695 | toastr.warning(t`Can't peek a character while group reply is being generated`); | 1723 | toastr.warning(t`Can't peek a character while group reply is being generated`); |
| 1696 | console.warn('Can\'t peek a character def while group reply is being generated'); | 1724 | console.warn('Can\'t peek a character def while group reply is being generated'); |
| @@ -1703,6 +1731,7 @@ function openCharacterDefinition(characterSelect) { | |||
| 1703 | return; | 1731 | return; |
| 1704 | } | 1732 | } |
| 1705 | 1733 | ||
| 1734 | await unshallowCharacter(chid); | ||
| 1706 | setCharacterId(chid); | 1735 | setCharacterId(chid); |
| 1707 | select_selected_character(chid); | 1736 | select_selected_character(chid); |
| 1708 | // Gentle nudge to recalculate tokens | 1737 | // Gentle nudge to recalculate tokens |
| @@ -314,6 +314,7 @@ let power_user = { | |||
| 314 | forbid_external_media: true, | 314 | forbid_external_media: true, |
| 315 | external_media_allowed_overrides: [], | 315 | external_media_allowed_overrides: [], |
| 316 | external_media_forbidden_overrides: [], | 316 | external_media_forbidden_overrides: [], |
| 317 | shallow_characters: false, | ||
| 317 | }; | 318 | }; |
| 318 | 319 | ||
| 319 | let themes = []; | 320 | let themes = []; |
| @@ -1595,6 +1596,7 @@ async function loadPowerUserSettings(settings, data) { | |||
| 1595 | $('#auto-connect-checkbox').prop('checked', power_user.auto_connect); | 1596 | $('#auto-connect-checkbox').prop('checked', power_user.auto_connect); |
| 1596 | $('#auto-load-chat-checkbox').prop('checked', power_user.auto_load_chat); | 1597 | $('#auto-load-chat-checkbox').prop('checked', power_user.auto_load_chat); |
| 1597 | $('#forbid_external_media').prop('checked', power_user.forbid_external_media); | 1598 | $('#forbid_external_media').prop('checked', power_user.forbid_external_media); |
| 1599 | $('#shallow_characters').prop('checked', power_user.shallow_characters); | ||
| 1598 | 1600 | ||
| 1599 | for (const theme of themes) { | 1601 | for (const theme of themes) { |
| 1600 | const option = document.createElement('option'); | 1602 | const option = document.createElement('option'); |
| @@ -3888,6 +3890,12 @@ $(document).ready(() => { | |||
| 3888 | await exportTheme(); | 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 | $(document).on('click', '#debug_table [data-debug-function]', function () { | 3899 | $(document).on('click', '#debug_table [data-debug-function]', function () { |
| 3892 | const functionId = $(this).data('debug-function'); | 3900 | const functionId = $(this).data('debug-function'); |
| 3893 | const functionRecord = debug_functions.find(f => f.functionId === functionId); | 3901 | const functionRecord = debug_functions.find(f => f.functionId === functionId); |
| @@ -47,6 +47,7 @@ import { | |||
| 47 | updateMessageBlock, | 47 | updateMessageBlock, |
| 48 | printMessages, | 48 | printMessages, |
| 49 | clearChat, | 49 | clearChat, |
| 50 | unshallowCharacter, | ||
| 50 | } from '../script.js'; | 51 | } from '../script.js'; |
| 51 | import { | 52 | import { |
| 52 | extension_settings, | 53 | extension_settings, |
| @@ -55,7 +56,7 @@ import { | |||
| 55 | renderExtensionTemplateAsync, | 56 | renderExtensionTemplateAsync, |
| 56 | writeExtensionField, | 57 | writeExtensionField, |
| 57 | } from './extensions.js'; | 58 | } from './extensions.js'; |
| 58 | import { groups, openGroupChat, selected_group } from './group-chats.js'; | 59 | import { groups, openGroupChat, selected_group, unshallowGroupMembers } from './group-chats.js'; |
| 59 | import { addLocaleData, getCurrentLocale, t, translate } from './i18n.js'; | 60 | import { addLocaleData, getCurrentLocale, t, translate } from './i18n.js'; |
| 60 | import { hideLoader, showLoader } from './loader.js'; | 61 | import { hideLoader, showLoader } from './loader.js'; |
| 61 | import { MacrosParser } from './macros.js'; | 62 | import { MacrosParser } from './macros.js'; |
| @@ -210,6 +211,8 @@ export function getContext() { | |||
| 210 | clearChat, | 211 | clearChat, |
| 211 | ChatCompletionService, | 212 | ChatCompletionService, |
| 212 | TextCompletionService, | 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 | * processCharacter - Process a given character, read its data and calculate its statistics. | 233 | * processCharacter - Process a given character, read its data and calculate its statistics. |
| 205 | * | 234 | * |
| 206 | * @param {string} item The name of the character. | 235 | * @param {string} item The name of the character. |
| 207 | * @param {import('../users.js').UserDirectoryList} directories User directories | 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 | * @return {Promise<object>} A Promise that resolves when the character processing is done. | 239 | * @return {Promise<object>} A Promise that resolves when the character processing is done. |
| 209 | */ | 240 | */ |
| 210 | const processCharacter = async (item, directories) => { | 241 | const processCharacter = async (item, directories, { shallow }) => { |
| 211 | try { | 242 | try { |
| 212 | const imgFile = path.join(directories.characters, item); | 243 | const imgFile = path.join(directories.characters, item); |
| 213 | const imgData = await readCharacterData(imgFile); | 244 | const imgData = await readCharacterData(imgFile); |
| @@ -226,7 +257,7 @@ const processCharacter = async (item, directories) => { | |||
| 226 | character['chat_size'] = chatSize; | 257 | character['chat_size'] = chatSize; |
| 227 | character['date_last_chat'] = dateLastChat; | 258 | character['date_last_chat'] = dateLastChat; |
| 228 | character['data_size'] = calculateDataSize(jsonObject?.data); | 259 | character['data_size'] = calculateDataSize(jsonObject?.data); |
| 229 | return character; | 260 | return shallow ? toShallow(character) : character; |
| 230 | } | 261 | } |
| 231 | catch (err) { | 262 | catch (err) { |
| 232 | console.error(`Could not process character: ${item}`); | 263 | console.error(`Could not process character: ${item}`); |
| @@ -991,9 +1022,10 @@ router.post('/delete', jsonParser, validateAvatarUrlMiddleware, async function ( | |||
| 991 | */ | 1022 | */ |
| 992 | router.post('/all', jsonParser, async function (request, response) { | 1023 | router.post('/all', jsonParser, async function (request, response) { |
| 993 | try { | 1024 | try { |
| 1025 | const shallow = !!request.body.shallow; | ||
| 994 | const files = fs.readdirSync(request.user.directories.characters); | 1026 | const files = fs.readdirSync(request.user.directories.characters); |
| 995 | const pngFiles = files.filter(file => file.endsWith('.png')); | 1027 | const pngFiles = files.filter(file => file.endsWith('.png')); |
| 996 | const processingPromises = pngFiles.map(file => processCharacter(file, request.user.directories)); | 1028 | const processingPromises = pngFiles.map(file => processCharacter(file, request.user.directories, { shallow })); |
| 997 | const data = (await Promise.all(processingPromises)).filter(c => c.name); | 1029 | const data = (await Promise.all(processingPromises)).filter(c => c.name); |
| 998 | return response.send(data); | 1030 | return response.send(data); |
| 999 | } catch (err) { | 1031 | } catch (err) { |
| @@ -1012,7 +1044,7 @@ router.post('/get', jsonParser, validateAvatarUrlMiddleware, async function (req | |||
| 1012 | return response.sendStatus(404); | 1044 | return response.sendStatus(404); |
| 1013 | } | 1045 | } |
| 1014 | 1046 | ||
| 1015 | const data = await processCharacter(item, request.user.directories); | 1047 | const data = await processCharacter(item, request.user.directories, { shallow: false }); |
| 1016 | 1048 | ||
| 1017 | return response.send(data); | 1049 | return response.send(data); |
| 1018 | } catch (err) { | 1050 | } catch (err) { |