Update and fix default expressions + emojis - Move "use default emojis" from its own toggle into the fallback dropdown - Add "no fallback" to the fallback dropdown - fix fallback expressions not being consistently used - Switching fallback will now reroll/reset the relevant expression - Add html attributes on the expression image

fb2ee756dd2db7fe7441c5acb6733aa1db126979

Wolfsblvt <wolfsblvt@gmail.com>

2 files changed, +132 -89Ignore whitespace
public/scripts/extensions/expressions/index.js+132 -85
@@ -70,6 +70,12 @@ const DEFAULT_EXPRESSIONS = [
7070 'surprise',
7171 'neutral',
7272];
73+
74+const OPTION_NO_FALLBACK = '#none';
75+const OPTION_EMOJI_FALLBACK = '#emoji';
76+const RESET_SPRITE_LABEL = '#reset';
77+
78+
7379/** @enum {number} */
7480const EXPRESSION_API = {
7581 local: 0,
@@ -106,15 +112,6 @@ function getPlaceholderImage(expression, isCustom = false) {
106112 };
107113}
108114
109-
110-/**
111- * Returns the fallback expression if explicitly chosen, otherwise the default one
112- * @returns {string} expression name
113- */
114-function getFallbackExpression() {
115- return extension_settings.expressions.fallback_expression ?? DEFAULT_FALLBACK_EXPRESSION;
116-}
117-
118115function isVisualNovelMode() {
119116 return Boolean(!isMobile() && power_user.waifuMode && getContext().groupId);
120117}
@@ -127,12 +124,12 @@ async function forceUpdateVisualNovelMode() {
127124
128125const updateVisualNovelModeDebounced = debounce(forceUpdateVisualNovelMode, debounce_timeout.quick);
129126
130127async function updateVisualNovelMode(namespriteFolderName, expression) {
131128 const vnContainer = $('#visual-novel-wrapper');
132129
133130 await visualNovelRemoveInactive(vnContainer);
134131
135132 const setSpritePromises = await visualNovelSetCharacterSprites(vnContainer, namespriteFolderName, expression);
136133
137134 // calculate layer indices based on recent messages
138135 await visualNovelUpdateLayers(vnContainer);
@@ -414,23 +411,6 @@ async function setImage(img, path) {
414411 });
415412}
416413
417-function onExpressionsShowDefaultInput() {
418- const value = $(this).prop('checked');
419- extension_settings.expressions.showDefault = value;
420- saveSettingsDebounced();
421-
422- const existingImageSrc = $('img.expression').prop('src');
423- if (existingImageSrc !== undefined) { //if we have an image in src
424- if (!value && existingImageSrc.includes('/img/default-expressions/')) { //and that image is from /img/ (default)
425- $('img.expression').prop('src', ''); //remove it
426- lastMessage = null;
427- }
428- if (value) {
429- lastMessage = null;
430- }
431- }
432-}
433-
434414async function moduleWorker() {
435415 const context = getContext();
436416
@@ -546,8 +526,8 @@ async function moduleWorker() {
546526 const force = !!context.groupId;
547527
548528 // Character won't be angry on you for swiping
549529 if (currentLastMessage.mes == '...' && expressionsList.includes(getFallbackExpression()extension_settings.expressions.fallback_expression)) {
550530 expression = getFallbackExpression()extension_settings.expressions.fallback_expression;
551531 }
552532
553533 await sendExpressionCall(spriteFolderName, expression, { force: force, vnMode: vnMode });
@@ -599,23 +579,23 @@ function getFolderNameByMessage(message) {
599579/**
600580 * Update the expression for the given character.
601581 *
602582 * @param {string} namespriteFolderName The character name, optionally with a sprite folder override, e.g. "folder/expression".
603583 * @param {string} expression The expression label, e.g. "amusement", "joy", etc.
604584 * @param {Object} [options] Additional options
605585 * @param {boolean} [options.force=false] If true, the expression will be sent even if it is the same as the current expression.
606586 * @param {boolean} [options.vnMode=null] If true, the expression will be sent in Visual Novel mode. If null, it will be determined by the current chat mode.
607587 * @param {string?} [options.overrideSpriteFile=null] - Set if a specific sprite file should be used. Must be sprite file name.
608588 */
609589async function sendExpressionCall(namespriteFolderName, expression, { force = false, vnMode = null, overrideSpriteFile = null } = {}) {
610590 lastExpression[namespriteFolderName.split('/')[0]] = expression;
611591 if (vnMode === null) {
612592 vnMode = isVisualNovelMode();
613593 }
614594
615595 if (vnMode) {
616596 await updateVisualNovelMode(namespriteFolderName, expression);
617597 } else {
618598 setExpression(namespriteFolderName, expression, { force: force, overrideSpriteFile: overrideSpriteFile });
619599 }
620600}
621601
@@ -679,7 +659,7 @@ async function setSpriteSlashCommand({ type }, searchTerm) {
679659 await validateImages(spriteFolderName);
680660
681661 // Handle reset as a special term and just reset the sprite via expression call
682662 if (searchTerm === '#reset'RESET_SPRITE_LABEL) {
683663 await sendExpressionCall(spriteFolderName, label, { force: true });
684664 return lastExpression[spriteFolderName] ?? '';
685665 }
@@ -933,12 +913,12 @@ function onTextGenSettingsReady(args) {
933913 * @param {EXPRESSION_API} [expressionsApi=extension_settings.expressions.api] - The expressions API to use for classification.
934914 * @param {object} [options={}] - Optional arguments.
935915 * @param {string?} [options.customPrompt=null] - The custom prompt to use for classification.
936916 * @returns {Promise<string?>} - The label of the expression.
937917 */
938918export async function getExpressionLabel(text, expressionsApi = extension_settings.expressions.api, { customPrompt = null } = {}) {
939919 // Return if text is undefined, saving a costly fetch request
940920 if ((!modules.includes('classify') && expressionsApi == EXPRESSION_API.extras) || !text) {
941921 return getFallbackExpression()extension_settings.expressions.fallback_expression;
942922 }
943923
944924 if (extension_settings.expressions.translate && typeof window['translate'] === 'function') {
@@ -968,7 +948,7 @@ export async function getExpressionLabel(text, expressionsApi = extension_settin
968948 await waitUntilCondition(() => online_status !== 'no_connection', 3000, 250);
969949 } catch (error) {
970950 console.warn('No LLM connection. Using fallback expression', error);
971951 return getFallbackExpression()extension_settings.expressions.fallback_expression;
972952 }
973953
974954 const expressionsList = await getExpressionsList();
@@ -981,7 +961,7 @@ export async function getExpressionLabel(text, expressionsApi = extension_settin
981961 case EXPRESSION_API.webllm: {
982962 if (!isWebLlmSupported()) {
983963 console.warn('WebLLM is not supported. Using fallback expression');
984964 return getFallbackExpression()extension_settings.expressions.fallback_expression;
985965 }
986966
987967 const expressionsList = await getExpressionsList();
@@ -1014,9 +994,9 @@ export async function getExpressionLabel(text, expressionsApi = extension_settin
1014994 } break;
1015995 }
1016996 } catch (error) {
1017997 toastr.infoerror('Could not classify expression. Check the console or your backend for more information.');
1018998 console.error(error);
1019999 return getFallbackExpression()extension_settings.expressions.fallback_expression;
10201000 }
10211001}
10221002
@@ -1231,13 +1211,20 @@ async function renderFallbackExpressionPicker() {
12311211 const defaultPicker = $('#expression_fallback');
12321212 defaultPicker.empty();
12331213
1234- const fallbackExpression = getFallbackExpression();
1214+
1215+ addOption(OPTION_NO_FALLBACK, '[ No fallback ]', !extension_settings.expressions.fallback_expression);
1216+ addOption(OPTION_EMOJI_FALLBACK, '[ Default emojis ]', !!extension_settings.expressions.showDefault);
12351217
12361218 for (const expression of expressions) {
1219+ addOption(expression, expression, expression == extension_settings.expressions.fallback_expression);
1220+ }
1221+
1222+ /** @type {(value: string, label: string, isSelected: boolean) => void} */
1223+ function addOption(value, label, isSelected) {
12371224 const option = document.createElement('option');
12381225 option.value = expressionvalue;
12391226 option.text = expressionlabel;
12401227 option.selected = expression == fallbackExpressionisSelected;
12411228 defaultPicker.append(option);
12421229 }
12431230}
@@ -1326,12 +1313,19 @@ export async function getExpressionsList() {
13261313 * @param {object} [options] - Options to select the sprite
13271314 * @param {string} [options.prevExpressionSrc=null] - The source of the previous expression
13281315 * @param {string} [options.overrideSpriteFile=null] - The file name of the sprite to select
13291316 * @returns {ExpressionImage?} - The selected sprite
13301317 */
13311318function chooseSpriteForExpression(spriteFolderName, expression, { prevExpressionSrc = null, overrideSpriteFile = null } = {}) {
1332- const sprite = (spriteCache[spriteFolderName] && spriteCache[spriteFolderName].find(x => x.label === expression));
1319+ if (!spriteCache[spriteFolderName]) return null;
1333- if (!(sprite?.files.length > 0))
1320+ if (expression === RESET_SPRITE_LABEL) return null;
1334- return null;
1321+
1322+ // Search for sprites of that expression - or fallback expression sprites if enabled
1323+ let sprite = spriteCache[spriteFolderName].find(x => x.label === expression);
1324+ if (!(sprite?.files.length > 0) && extension_settings.expressions.fallback_expression) {
1325+ sprite = spriteCache[spriteFolderName].find(x => x.label === extension_settings.expressions.fallback_expression);
1326+ console.debug('Expression', expression, 'not found. Using fallback expression', extension_settings.expressions.fallback_expression);
1327+ }
1328+ if (!(sprite?.files.length > 0)) return null;
13351329
13361330 let spriteFile = sprite.files[0];
13371331
@@ -1397,6 +1391,10 @@ async function setExpression(spriteFolderName, expression, { force = false, over
13971391 expressionClone.attr('id', '').css({ opacity: 0 });
13981392 //add new sprite path to clone src
13991393 expressionClone.attr('src', spriteFile.imageSrc);
1394+ //set relevant data tags
1395+ expressionClone.attr('data-sprite-folder-name', spriteFolderName);
1396+ expressionClone.attr('data-expression', expression);
1397+ expressionClone.attr('data-sprite-filename', spriteFile.fileName);
14001398 //add invisible clone to html
14011399 expressionClone.appendTo($('#expression-holder'));
14021400
@@ -1442,12 +1440,12 @@ async function setExpression(spriteFolderName, expression, { force = false, over
14421440
14431441 expressionClone.removeClass('default');
14441442 expressionClone.off('error');
14451443 expressionClone.on('error', function (error) {
14461444 console.debug('Expression image error', spriteFile.imageSrc, error);
14471445 $(this).attr('src', '');
14481446 $(this).off('error');
14491447 if (force && extension_settings.expressions.showDefault) {
14501448 setDefaultsetDefaultEmojiForImage(img, expression);
14511449 }
14521450 });
14531451 }
@@ -1455,28 +1453,50 @@ async function setExpression(spriteFolderName, expression, { force = false, over
14551453 console.info('Expression set', { expression: spriteFile.expression, file: spriteFile.fileName });
14561454 }
14571455 else {
1458- if (extension_settings.expressions.showDefault) {
1456+ img.attr('data-sprite-folder-name', spriteFolderName);
1459- setDefault();
1457+ img.attr('data-expression', expression);
1458+
1459+ $(img).off('error');
1460+
1461+ if (extension_settings.expressions.showDefault && expression !== RESET_SPRITE_LABEL) {
1462+ setDefaultEmojiForImage(img, expression);
14601463 } else {
14611464 setNonesetNoneForImage(img, expression);
14621465 }
14631466 console.debug('Expression unset - No sprite found', { expression: expression });
14641467 }
14651468
1466- function setDefault() {
1469+ document.getElementById('expression-holder').style.display = '';
1467- console.debug('setting default expression');
1470+}
1468- const defImgUrl = `/img/default-expressions/${expression}.png`;
1471+
1469- //console.log(defImgUrl);
1472+/**
1470- img.attr('src', defImgUrl);
1473+ * Sets the default expression image for the given image element and expression
1471- img.addClass('default');
1474+ * @param {JQuery<HTMLElement>} img - The image element to set the default expression for
1472- }
1475+ * @param {string} expression - The expression label to use for the default image
1473- function setNone() {
1476+ */
1474- console.debug('setting no expression');
1477+function setDefaultEmojiForImage(img, expression) {
1475- img.attr('src', '');
1478+ if (extension_settings.expressions.custom?.includes(expression)) {
1476- img.removeClass('default');
1479+ console.debug(`Can't set default emoji for a custom expression (${expression}). setting to ${DEFAULT_FALLBACK_EXPRESSION} instead.`);
1480+ expression = DEFAULT_FALLBACK_EXPRESSION;
14771481 }
14781482
1479- document.getElementById('expression-holder').style.display = '';
1483+ const defImgUrl = `/img/default-expressions/${expression}.png`;
1484+ img.attr('src', defImgUrl);
1485+ img.attr('data-expression', expression);
1486+ img.attr('data-sprite-filename', null);
1487+ img.addClass('default');
1488+}
1489+
1490+/**
1491+ * Sets the image element to display no expression by clearing its source attribute.
1492+ * @param {JQuery<HTMLElement>} img - The image element to clear the expression for
1493+ * @param {string} expression - The expression label to use
1494+ */
1495+function setNoneForImage(img, expression) {
1496+ img.attr('src', '');
1497+ img.attr('data-expression', expression);
1498+ img.attr('data-sprite-filename', null);
1499+ img.removeClass('default');
14801500}
14811501
14821502function onClickExpressionImage() {
@@ -1504,19 +1524,15 @@ async function onClickExpressionAddCustom() {
15041524
15051525 // a-z, 0-9, dashes and underscores only
15061526 if (!/^[a-z0-9-_]+$/.test(expressionName)) {
15071527 toastr.infowarning('Invalid custom expression name provided', 'Add Custom Expression');
15081528 return;
15091529 }
1510-
1511- // Check if expression name already exists in default expressions
15121530 if (DEFAULT_EXPRESSIONS.includes(expressionName)) {
15131531 toastr.infowarning('Expression name already exists', 'Add Custom Expression');
15141532 return;
15151533 }
1516-
1517- // Check if expression name already exists in custom expressions
15181534 if (extension_settings.expressions.custom.includes(expressionName)) {
15191535 toastr.infowarning('Custom expression already exists', 'Add Custom Expression');
15201536 return;
15211537 }
15221538
@@ -1550,8 +1566,8 @@ async function onClickExpressionRemoveCustom() {
15501566 // Remove custom expression from settings
15511567 const index = extension_settings.expressions.custom.indexOf(selectedExpression);
15521568 extension_settings.expressions.custom.splice(index, 1);
15531569 if (selectedExpression == getFallbackExpression()extension_settings.expressions.fallback_expression) {
15541570 toastr.warning(`Deleted custom expression '${selectedExpression}' that was also selected as the fallback expression.\nFallback expression has been reset to '${DEFAULT_FALLBACK_EXPRESSION}'.`, 'Remove Custom Expression');
15551571 extension_settings.expressions.fallback_expression = DEFAULT_FALLBACK_EXPRESSION;
15561572 }
15571573 await renderAdditionalExpressionSettings();
@@ -1575,12 +1591,35 @@ function onExpressionApiChanged() {
15751591 }
15761592}
15771593
15781594async function onExpressionFallbackChanged() {
1579- const expression = this.value;
1595+ /** @type {HTMLSelectElement} */
1580- if (expression) {
1596+ const select = this;
1581- extension_settings.expressions.fallback_expression = expression;
1597+ const selectedValue = select.value;
1582- saveSettingsDebounced();
1598+
1599+ switch (selectedValue) {
1600+ case OPTION_NO_FALLBACK:
1601+ extension_settings.expressions.fallback_expression = null;
1602+ extension_settings.expressions.showDefault = false;
1603+ break;
1604+ case OPTION_EMOJI_FALLBACK:
1605+ extension_settings.expressions.fallback_expression = null;
1606+ extension_settings.expressions.showDefault = true;
1607+ break;
1608+ default:
1609+ extension_settings.expressions.fallback_expression = selectedValue;
1610+ extension_settings.expressions.showDefault = false;
1611+ break;
15831612 }
1613+
1614+ const img = $('img.expression');
1615+ const spriteFolderName = img.attr('data-sprite-folder-name');
1616+ const expression = img.attr('data-expression');
1617+
1618+ if (spriteFolderName && expression) {
1619+ await sendExpressionCall(spriteFolderName, expression, { force: true });
1620+ }
1621+
1622+ saveSettingsDebounced();
15841623}
15851624
15861625async function handleFileUpload(url, formData) {
@@ -1924,6 +1963,16 @@ function migrateSettings() {
19241963 extension_settings.expressions.llmPrompt = DEFAULT_LLM_PROMPT;
19251964 saveSettingsDebounced();
19261965 }
1966+
1967+ if (extension_settings.expressions.allowMultiple === undefined) {
1968+ extension_settings.expressions.allowMultiple = true;
1969+ saveSettingsDebounced();
1970+ }
1971+
1972+ if (extension_settings.expressions.showDefault && extension_settings.expressions.fallback_expression !== undefined) {
1973+ extension_settings.expressions.showDefault = false;
1974+ saveSettingsDebounced();
1975+ }
19271976}
19281977
19291978(async function () {
@@ -1950,9 +1999,7 @@ function migrateSettings() {
19501999 const template = await renderExtensionTemplateAsync(MODULE_NAME, 'settings');
19512000 $('#expressions_container').append(template);
19522001 $('#expression_override_button').on('click', onClickExpressionOverrideButton);
1953- $('#expressions_show_default').on('input', onExpressionsShowDefaultInput);
19542002 $('#expression_upload_pack_button').on('click', onClickExpressionUploadPackButton);
1955- $('#expressions_show_default').prop('checked', extension_settings.expressions.showDefault).trigger('input');
19562003 $('#expression_translate').prop('checked', extension_settings.expressions.translate).on('input', function () {
19572004 extension_settings.expressions.translate = !!$(this).prop('checked');
19582005 saveSettingsDebounced();
@@ -2080,7 +2127,7 @@ function migrateSettings() {
20802127 if (type == 'sprite') return localEnumProviders.sprites();
20812128 else return [
20822129 ...localEnumProviders.expressions(),
20832130 new SlashCommandEnumValue('#reset'RESET_SPRITE_LABEL, 'Resets the expression (to either default or no sprite)', enumTypes.enum, '❌'),
20842131 ];
20852132 },
20862133 }),
public/scripts/extensions/expressions/settings.html+0 -4
@@ -10,10 +10,6 @@
1010 <input id="expression_translate" type="checkbox">
1111 <span data-i18n="Translate text to English before classification">Translate text to English before classification</span>
1212 </label>
13- <label class="checkbox_label" for="expressions_show_default">
14- <input id="expressions_show_default" type="checkbox">
15- <span data-i18n="Show default images (emojis) if sprite missing">Show default images (emojis) if sprite missing</span>
16- </label>
1713 <label class="checkbox_label" for="expressions_allow_multiple" title="A single expression can have multiple sprites. Whenever the expression is chosen, a random sprite for this expression will be selected." data-i18n="[title]A single expression can have multiple sprites. Whenever the expression is chosen, a random sprite for this expression will be selected.">
1814 <input id="expressions_allow_multiple" type="checkbox">
1915 <span data-i18n="Allow multiple sprites per expression">Allow multiple sprites per expression</span>