Add shallow character load mode

383806325a0dca99ad4b4fa3f257c868600ff0ab

Cohee <18619528+Cohee1207@users.noreply.github.com>

6 files changed, +125 -10Showing whitespace changes
public/index.html+7 -0
@@ -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>
public/script.js+37 -1
@@ -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();
36603660
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 execution3664 // 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);
36633666
@@ -6704,9 +6707,42 @@ export function buildAvatarList(block, entities, { templateId = 'inline_avatar_t
6704 }6707 }
6705}6708}
67066709
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 */
6715export 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
6707export async function getChat() {6741export 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',
public/scripts/group-chats.js+33 -4
@@ -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';
76import { printTagList, createTagMapFromList, applyTagsOnCharacterSelect, tag_map, applyTagsOnGroupSelect } from './tags.js';77import { printTagList, createTagMapFromList, applyTagsOnCharacterSelect, tag_map, applyTagsOnGroupSelect } from './tags.js';
77import { FILTER_TYPES, FilterHelper } from './filters.js';78import { FILTER_TYPES, FilterHelper } from './filters.js';
@@ -216,6 +217,7 @@ export async function getGroupChat(groupId, reload = false) {
216217
217 // Run validation before any loading218 // Run validation before any loading
218 validateGroup(group);219 validateGroup(group);
220 await unshallowGroupMembers(groupId);
219221
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 }
825827
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}
11391143
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 */
1149export 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
1140let groupAutoModeAbortController = null;1167let groupAutoModeAbortController = null;
11411168
1142async function groupChatAutoModeWorker() {1169async 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}
11601187
1161async function modifyGroupMember(chat_id, groupMember, isDelete) {1188async 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;
11651192
1166 if (isDelete) {1193 if (isDelete) {
@@ -1173,6 +1200,7 @@ async function modifyGroupMember(chat_id, groupMember, isDelete) {
1173 }1200 }
11741201
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 }
16391667
1640 if (action === 'view') {1668 if (action === 'view') {
1641 openCharacterDefinition(member);1669 await openCharacterDefinition(member);
1642 }1670 }
16431671
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}
16921720
1693function openCharacterDefinition(characterSelect) {1721async 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 }
17051733
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 tokens1737 // Gentle nudge to recalculate tokens
public/scripts/power-user.js+8 -0
@@ -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};
318319
319let themes = [];320let 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);
15981600
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 });
38903892
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);
public/scripts/st-context.js+4 -1
@@ -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';
51import {52import {
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';
58import { groups, openGroupChat, selected_group } from './group-chats.js';59import { groups, openGroupChat, selected_group, unshallowGroupMembers } from './group-chats.js';
59import { addLocaleData, getCurrentLocale, t, translate } from './i18n.js';60import { addLocaleData, getCurrentLocale, t, translate } from './i18n.js';
60import { hideLoader, showLoader } from './loader.js';61import { hideLoader, showLoader } from './loader.js';
61import { MacrosParser } from './macros.js';62import { 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}
215218
src/endpoints/characters.js+36 -4
@@ -201,13 +201,44 @@ const calculateDataSize = (data) => {
201};201};
202202
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 */
208const 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 directories236 * @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 */
210const processCharacter = async (item, directories) => {241const 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 */
992router.post('/all', jsonParser, async function (request, response) {1023router.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 }
10141046
1015 const data = await processCharacter(item, request.user.directories);1047 const data = await processCharacter(item, request.user.directories, { shallow: false });
10161048
1017 return response.send(data);1049 return response.send(data);
1018 } catch (err) {1050 } catch (err) {