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>
Signed| @@ -2367,7 +2367,7 @@ export function findChar({ name = null, allowAvatar = true, insensitive = true, | |||
| 2367 | 2367 | ||
| 2368 | // If allowAvatar is true, search by avatar first | 2368 | // 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 | } |
| @@ -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; | ||
| 1127 | 1128 | ||
| 1128 | const entries = await getEntriesFromFile(file); | 1129 | const entries = await getEntriesFromFile(file); |
| 1129 | 1130 | ||
| @@ -1143,7 +1144,31 @@ function registerWorldInfoSlashCommands() { | |||
| 1143 | return ''; | 1144 | return ''; |
| 1144 | } | 1145 | } |
| 1145 | 1146 | ||
| 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 | } | ||
| 1147 | 1172 | ||
| 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 | }; | ||
| 1192 | 1234 | ||
| 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 | } |
| 1218 | 1260 | ||
| 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 | } | ||
| 1228 | 1296 | ||
| 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 | } |
| 1232 | 1301 | ||
| 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))), |
| 1354 | 1423 | ||
| 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 | }; |
| 3582 | 3654 | ||
| 3583 | export const newWorldInfoEntryTemplate = Object.fromEntries( | 3655 | export const newWorldInfoEntryTemplate = Object.fromEntries( |
| @@ -863,8 +863,7 @@ router.post('/search', validateAvatarUrlMiddleware, function (request, response) | |||
| 863 | 863 | ||
| 864 | // Search through title and messages of the chat | 864 | // 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)); |
| 869 | 868 | ||
| 870 | if (hasMatch) { | 869 | if (hasMatch) { |