Add support for characterFilter to getEntryField and setEntryField (#4185) * Add support for characterFilter to getEntryField and setEntryField * No more conflicts between lint and autoformat * Fix array type default values hint * Properly capitalize name * Fix search by name --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>

116bef6c9ffbe2c912cd883a15aad7554ecbc4c6

Wandering Mouse <215439289+WanderingMouse@users.noreply.github.com>

Signed
3 files changed, +87 -16Ignore whitespace
public/scripts/utils.js+1 -1
@@ -2367,7 +2367,7 @@ export function findChar({ name = null, allowAvatar = true, insensitive = true,
23672367
2368 // If allowAvatar is true, search by avatar first2368 // If allowAvatar is true, search by avatar first
2369 if (allowAvatar && name) {2369 if (allowAvatar && name) {
2370 const characterByAvatar = filteredCharacters.find(char => char.avatar === name);2370 const characterByAvatar = filteredCharacters.find(char => char.avatar === name || (!name.endsWith('.png') && char.avatar === `${name}.png`));
2371 if (characterByAvatar) {2371 if (characterByAvatar) {
2372 return characterByAvatar;2372 return characterByAvatar;
2373 }2373 }
public/scripts/world-info.js+85 -13
@@ -1124,6 +1124,7 @@ function registerWorldInfoSlashCommands() {
1124 async function getEntryFieldCallback(args, uid) {1124 async function getEntryFieldCallback(args, uid) {
1125 const file = args.file;1125 const file = args.file;
1126 const field = args.field || 'content';1126 const field = args.field || 'content';
1127 const tags = getContext().tags;
11271128
1128 const entries = await getEntriesFromFile(file);1129 const entries = await getEntriesFromFile(file);
11291130
@@ -1143,7 +1144,31 @@ function registerWorldInfoSlashCommands() {
1143 return '';1144 return '';
1144 }1145 }
11451146
1146 const fieldValue = entry[field];1147 // handle special cases, otherwise execute default logic
1148 let fieldValue;
1149 switch (field){
1150 case 'characterFilterNames':
1151 if (entry.characterFilter) {
1152 fieldValue = entry.characterFilter.names;
1153 }
1154 break;
1155 case 'characterFilterTags':
1156 if (entry.characterFilter) {
1157 if (!entry.characterFilter.tags) {
1158 return '';
1159 }
1160 //Find the tag objects corresponding to each ID in the array, then return the names
1161 fieldValue = tags.filter((tag) => entry.characterFilter.tags.includes(tag.id)).map((tag) => tag.name);
1162 }
1163 break;
1164 case 'characterFilterExclude':
1165 if (entry.characterFilter) {
1166 fieldValue = entry.characterFilter.isExclude;
1167 }
1168 break;
1169 default:
1170 fieldValue = entry[field];
1171 }
11471172
1148 if (fieldValue === undefined) {1173 if (fieldValue === undefined) {
1149 return '';1174 return '';
@@ -1189,6 +1214,23 @@ function registerWorldInfoSlashCommands() {
1189 const file = args.file;1214 const file = args.file;
1190 const uid = args.uid;1215 const uid = args.uid;
1191 const field = args.field || 'content';1216 const field = args.field || 'content';
1217 const tags = getContext().tags;
1218
1219 // characterFilter is an object with internal fields we need to access, which may also may be null and need to be populated
1220 const createCharacterFilterFieldObjectIfNeeded = (currentEntry) => {
1221 if (!currentEntry.characterFilter) {
1222 Object.assign(
1223 currentEntry,
1224 {
1225 characterFilter: {
1226 isExclude: false,
1227 names: [],
1228 tags: [],
1229 },
1230 },
1231 );
1232 }
1233 };
11921234
1193 if (value === undefined) {1235 if (value === undefined) {
1194 toastr.warning('Value is required');1236 toastr.warning('Value is required');
@@ -1216,18 +1258,45 @@ function registerWorldInfoSlashCommands() {
1216 return '';1258 return '';
1217 }1259 }
12181260
1219 if (Array.isArray(entry[field])) {1261 // handle special cases, otherwise execute default logic
1220 entry[field] = parseStringArray(value);1262 let tagNames;
1221 } else if (typeof entry[field] === 'boolean') {1263 let charNames;
1222 entry[field] = isTrueBoolean(value);1264 switch (field){
1223 } else if (typeof entry[field] === 'number') {1265 case 'characterFilterNames':
1224 entry[field] = Number(value);1266 createCharacterFilterFieldObjectIfNeeded(entry);
1225 } else {1267 charNames = parseStringArray(value);
1226 entry[field] = value;1268 entry.characterFilter.names = charNames
1227 }1269 .map((name) => getCharaFilename(null, { manualAvatarKey: findChar({ name, allowAvatar: true, preferCurrentChar: false, quiet: true })?.avatar }))
1270 .filter(Boolean)
1271 .filter(onlyUnique);
1272 setWIOriginalDataValue(data, uid, 'character_filter', entry.characterFilter);
1273 break;
1274 case 'characterFilterTags':
1275 createCharacterFilterFieldObjectIfNeeded(entry);
1276 tagNames = parseStringArray(value);
1277 //Find the tag objects corresponding to each name in the user array, then return an array of the corresponding IDs
1278 entry.characterFilter.tags = tags.filter((tag) => tagNames.includes(tag.name)).map((tag) => tag.id);
1279 setWIOriginalDataValue(data, uid, 'character_filter', entry.characterFilter);
1280 break;
1281 case 'characterFilterExclude':
1282 createCharacterFilterFieldObjectIfNeeded(entry);
1283 entry.characterFilter.isExclude = isTrueBoolean(value);
1284 setWIOriginalDataValue(data, uid, 'character_filter', entry.characterFilter);
1285 break;
1286 default:
1287 if (Array.isArray(entry[field])) {
1288 entry[field] = parseStringArray(value);
1289 } else if (typeof entry[field] === 'boolean') {
1290 entry[field] = isTrueBoolean(value);
1291 } else if (typeof entry[field] === 'number') {
1292 entry[field] = Number(value);
1293 } else {
1294 entry[field] = value;
1295 }
12281296
1229 if (originalWIDataKeyMap[field]) {1297 if (originalWIDataKeyMap[field]) {
1230 setWIOriginalDataValue(data, uid, originalWIDataKeyMap[field], entry[field]);1298 setWIOriginalDataValue(data, uid, originalWIDataKeyMap[field], entry[field]);
1299 }
1231 }1300 }
12321301
1233 await saveWorldInfo(file, data);1302 await saveWorldInfo(file, data);
@@ -1349,7 +1418,7 @@ function registerWorldInfoSlashCommands() {
1349 const localEnumProviders = {1418 const localEnumProviders = {
1350 /** All possible fields that can be set in a WI entry */1419 /** All possible fields that can be set in a WI entry */
1351 wiEntryFields: () => Object.entries(newWorldInfoEntryDefinition).map(([key, value]) =>1420 wiEntryFields: () => Object.entries(newWorldInfoEntryDefinition).map(([key, value]) =>
1352 new SlashCommandEnumValue(key, `[${value.type}] default: ${(typeof value.default === 'string' ? `'${value.default}'` : value.default)}`,1421 new SlashCommandEnumValue(key, `[${value.type}] default: ${(typeof value.default === 'string' ? `'${value.default}'` : JSON.stringify(value.default))}`,
1353 enumTypes.enum, enumIcons.getDataTypeIcon(value.type))),1422 enumTypes.enum, enumIcons.getDataTypeIcon(value.type))),
13541423
1355 /** All existing UIDs based on the file argument as world name */1424 /** All existing UIDs based on the file argument as world name */
@@ -3578,6 +3647,9 @@ export const newWorldInfoEntryDefinition = {
3578 sticky: { default: null, type: 'number?' },3647 sticky: { default: null, type: 'number?' },
3579 cooldown: { default: null, type: 'number?' },3648 cooldown: { default: null, type: 'number?' },
3580 delay: { default: null, type: 'number?' },3649 delay: { default: null, type: 'number?' },
3650 characterFilterNames: { default: [], type: 'array' },
3651 characterFilterTags: { default: [], type: 'array' },
3652 characterFilterExclude: { default: false, type: 'boolean' },
3581};3653};
35823654
3583export const newWorldInfoEntryTemplate = Object.fromEntries(3655export const newWorldInfoEntryTemplate = Object.fromEntries(
src/endpoints/chats.js+1 -2
@@ -863,8 +863,7 @@ router.post('/search', validateAvatarUrlMiddleware, function (request, response)
863863
864 // Search through title and messages of the chat864 // Search through title and messages of the chat
865 const fragments = query.trim().toLowerCase().split(/\s+/).filter(x => x);865 const fragments = query.trim().toLowerCase().split(/\s+/).filter(x => x);
866 const text = [path.parse(chatFile.path).name,866 const text = [path.parse(chatFile.path).name, ...messages.map(message => message?.mes)].join('\n').toLowerCase();
867 ...messages.map(message => message?.mes)].join('\n').toLowerCase();
868 const hasMatch = fragments.every(fragment => text.includes(fragment));867 const hasMatch = fragments.every(fragment => text.includes(fragment));
869868
870 if (hasMatch) {869 if (hasMatch) {