Merge branch 'SillyTavern:staging' into staging

This commit is contained in:
Azariel Del Carmen
2024-05-14 10:47:08 +01:00
committed by GitHub
24 changed files with 318 additions and 74 deletions

View File

@@ -75,7 +75,8 @@
"postinstall": "node post-install.js",
"lint": "eslint \"src/**/*.js\" \"public/**/*.js\" ./*.js",
"lint:fix": "eslint \"src/**/*.js\" \"public/**/*.js\" ./*.js --fix",
"plugins:update": "node plugins update"
"plugins:update": "node plugins update",
"plugins:install": "node plugins install"
},
"bin": {
"sillytavern": "./server.js"

View File

@@ -15,6 +15,12 @@ if (command === 'update') {
updatePlugins();
}
if (command === 'install') {
const pluginName = process.argv[3];
console.log('Installing a new plugin', color.green(pluginName));
installPlugin(pluginName);
}
async function updatePlugins() {
const directories = fs.readdirSync(pluginsPath)
.filter(file => !file.startsWith('.'))
@@ -51,3 +57,19 @@ async function updatePlugins() {
console.log(color.magenta('All plugins updated!'));
}
async function installPlugin(pluginName) {
try {
const pluginPath = path.join(pluginsPath, path.basename(pluginName, '.git'));
if (fs.existsSync(pluginPath)) {
return console.log(color.yellow(`Directory already exists at ${pluginPath}`));
}
await git().clone(pluginName, pluginPath, { '--depth': 1 });
console.log(`Plugin ${color.green(pluginName)} installed to ${color.cyan(pluginPath)}`);
}
catch (error) {
console.error(color.red(`Failed to install plugin ${pluginName}`), error);
}
}

View File

@@ -1680,7 +1680,7 @@
</div>
</div>
<div class="range-block" data-source="openai,openrouter,makersuite,claude,custom">
<label for="openai_image_inlining" class="checkbox_label flexWrap widthFreeExpand">
<label for="openai_image_inlining" class="checkbox_label flexWrap widthFreeExpand marginBot10">
<input id="openai_image_inlining" type="checkbox" />
<span data-i18n="Send inline images">Send inline images</span>
<div id="image_inlining_hint" class="flexBasis100p toggle-description justifyLeft">
@@ -1689,6 +1689,16 @@
<code><i class="fa-solid fa-wand-magic-sparkles"></i></code> menu to attach an image file to the chat.
</div>
</label>
<div class="flex-container flexFlowColumn wide100p textAlignCenter">
<label for="openai_inline_image_quality">
Inline Image Quality
</label>
<select id="openai_inline_image_quality">
<option value="auto">Auto</option>
<option value="low">Low</option>
<option value="high">High</option>
</select>
</div>
</div>
<div class="range-block" data-source="ai21">
<label for="use_ai21_tokenizer" title="Use AI21 Tokenizer" class="checkbox_label widthFreeExpand">
@@ -2405,6 +2415,10 @@
<option value="gpt-4-32k-0613">gpt-4-32k-0613 (2023)</option>
<option value="gpt-4-32k-0314">gpt-4-32k-0314 (2023)</option>
</optgroup>
<optgroup label="GPT-4o">
<option value="gpt-4o">gpt-4o</option>
<option value="gpt-4o-2024-05-13">gpt-4o-2024-05-13</option>
</optgroup>
<optgroup label="GPT-4 Turbo">
<option value="gpt-4-turbo">gpt-4-turbo</option>
<option value="gpt-4-turbo-2024-04-09">gpt-4-turbo-2024-04-09</option>
@@ -4116,6 +4130,12 @@
</div>
<div name="AutoCompleteToggle">
<h4 data-i18n="AutoComplete Settings">AutoComplete Settings</h4>
<label data-newbie-hidden class="checkbox_label" for="stscript_autocomplete_autoHide">
<input id="stscript_autocomplete_autoHide" type="checkbox" />
<small data-i18n="Automatically hide details">
Automatically hide details
</small>
</label>
<div class="flex-container">
<div class="flex1" title="Determines how entries are found for autocomplete." data-i18n="[title]Determines how entries are found for autocomplete.">
<label for="stscript_matching" data-i18n="Autocomplete Matching"><small>Matching</small></label>
@@ -4171,14 +4191,14 @@
<label class="checkbox_label" title="Switch to stricter escaping, allowing all delimiting characters to be escaped with a backslash, and backslashes to be escaped as well." data-i18n="[title]Switch to stricter escaping, allowing all dellimiting characters to be escaped with a backslash, and backslashes to be escaped as well.">
<input id="stscript_parser_flag_strict_escaping" type="checkbox" />
<span data-i18n="STRICT_ESCAPING"><small>STRICT_ESCAPING</small></span>
<a href="https://docs.sillytavern.app/" target="_blank" class="notes-link">
<a href="https://docs.sillytavern.app/usage/st-script/#strict-escaping" target="_blank" class="notes-link">
<span class="fa-solid fa-circle-question note-link-span"></span>
</a>
</label>
<label class="checkbox_label" title="Replace all {{getvar::}} and {{getglobalvar::}} macros with scoped variables to avoid double macro substitution." data-i18n="[title]Replace all {{getvar::}} and {{getglobalvar::}} macros with scoped variables to avoid double macro substitution.">
<input id="stscript_parser_flag_replace_getvar" type="checkbox" />
<span data-i18n="REPLACE_GETVAR"><small>REPLACE_GETVAR</small></span>
<a href="https://docs.sillytavern.app/" target="_blank" class="notes-link">
<a href="https://docs.sillytavern.app/usage/st-script/#replace-variable-macros" target="_blank" class="notes-link">
<span class="fa-solid fa-circle-question note-link-span"></span>
</a>
</label>

View File

@@ -42,6 +42,46 @@ EventEmitter.prototype.on = function (event, listener) {
this.events[event].push(listener);
};
/**
* Makes the listener the last to be called when the event is emitted
* @param {string} event Event name
* @param {function} listener Event listener
*/
EventEmitter.prototype.makeLast = function (event, listener) {
if (typeof this.events[event] !== 'object') {
this.events[event] = [];
}
const events = this.events[event];
const idx = events.indexOf(listener);
if (idx > -1) {
events.splice(idx, 1);
}
events.push(listener);
}
/**
* Makes the listener the first to be called when the event is emitted
* @param {string} event Event name
* @param {function} listener Event listener
*/
EventEmitter.prototype.makeFirst = function (event, listener) {
if (typeof this.events[event] !== 'object') {
this.events[event] = [];
}
const events = this.events[event];
const idx = events.indexOf(listener);
if (idx > -1) {
events.splice(idx, 1);
}
events.unshift(listener);
}
EventEmitter.prototype.removeListener = function (event, listener) {
var idx;

View File

@@ -425,7 +425,7 @@
"Start new chat": "새로운 채팅 시작",
"View past chats": "과거 채팅 보기",
"Delete messages": "메시지 삭제",
"Impersonate": "사칭",
"Impersonate": "대신 말하기",
"Regenerate": "재생성",
"PNG": "PNG",
"JSON": "JSON",
@@ -914,7 +914,30 @@
"Learn how to contribute your idle GPU cycles to the Horde": "여유로운 GPU 주기를 호드에 기여하는 방법 배우기",
"Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "Google 모델용 적절한 토크나이저를 사용하여 API를 통해 제공됩니다. 더 느린 프롬프트 처리지만 훨씬 정확한 토큰 계산을 제공합니다.",
"Load koboldcpp order": "코볼드 CPP 순서로 로드",
"Use Google Tokenizer": "Google 토크나이저 사용"
"Use Google Tokenizer": "구글 토크나이저 사용",
"Hide Chat Avatars": "채팅 아바타 숨기기",
"Hide avatars in chat messages.": "채팅 메시지에서 아바타 숨김.",
"Avatar Hover Magnification": "아바타 마우스오버 시 확대",
"Enable magnification for zoomed avatar display.": "마우스 오버 시 아바타가 커지도록 설정하세요.",
"AutoComplete Settings": "자동 완성 설정",
"Autocomplete Matching": "자동 완성 매칭",
"Starts with": "시작하는 단어로",
"Autocomplete Style": "자동 완성 스타일",
"Includes": "포함하는",
"Fuzzy": "퍼지 매칭",
"Follow Theme": "테마 적용",
"Dark": "다크 모드",
"Sets the font size of the autocomplete.": "자동 완성 글꼴 크기 설정",
"Autocomplete Width": "자동 완성 너비 조절",
"Parser Flags": "파서 플래그 설정",
"Sets default flags for the STscript parser.": "STscript 파서 기본 플래그 설정",
"Switch to stricter escaping, allowing all delimiting characters to be escaped with a backslash, and backslashes to be escaped as well.": "모든 구분자를 백슬래시로 이스케이핑하고, 백슬래시 자체도 이스케이프할 수 있도록 엄격한 방식으로 전환합니다.",
"STscript Settings": "STscript 설정",
"Smooth Streaming": "부드러운 스트리밍",
"Experimental feature. May not work for all backends.": "실험적인 기능으로, 모든 백엔드에서 작동이 보장되지는 않을 수 있습니다.",
"Char List Subheader": "문자 목록 하위 제목",
"Account": "계정",
"Theme Colors": "테마 색상",
"# Messages to Load": "로딩할 메시지 수"
}

View File

@@ -702,7 +702,7 @@ const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
*/
function autoFitSendTextArea() {
const originalScrollBottom = chatBlock.scrollHeight - (chatBlock.scrollTop + chatBlock.offsetHeight);
if (sendTextArea.scrollHeight + 3 == sendTextArea.offsetHeight) {
if (Math.ceil(sendTextArea.scrollHeight + 3) >= Math.floor(sendTextArea.offsetHeight)) {
// Needs to be pulled dynamically because it is affected by font size changes
const sendTextAreaMinHeight = window.getComputedStyle(sendTextArea).getPropertyValue('min-height');
sendTextArea.style.height = sendTextAreaMinHeight;

View File

@@ -25,6 +25,8 @@ export class AutoComplete {
/**@type {boolean}*/ isReplaceable = false;
/**@type {boolean}*/ isShowingDetails = false;
/**@type {boolean}*/ wasForced = false;
/**@type {boolean}*/ isForceHidden = false;
/**@type {boolean}*/ canBeAutoHidden = false;
/**@type {string}*/ text;
/**@type {AutoCompleteNameResult}*/ parserResult;
@@ -57,6 +59,10 @@ export class AutoComplete {
return power_user.stscript.matching ?? 'fuzzy';
}
get autoHide() {
return power_user.stscript.autocomplete.autoHide ?? false;
}
@@ -224,6 +230,16 @@ export class AutoComplete {
return a.name.localeCompare(b.name);
}
basicAutoHideCheck() {
// auto hide only if at least one char has been typed after the name + space
return this.textarea.selectionStart > this.parserResult.start
+ this.parserResult.name.length
+ (this.startQuote ? 1 : 0)
+ (this.endQuote ? 1 : 0)
+ 1
;
}
/**
* Show the autocomplete.
* @param {boolean} isInput Whether triggered by input.
@@ -244,6 +260,9 @@ export class AutoComplete {
return this.hide();
}
// disable force-hide if trigger was forced
if (isForced) this.isForceHidden = false;
// request provider to get name result (potentially "incomplete", i.e. not an actual existing name) for
// cursor position
this.parserResult = await this.getNameAt(this.text, this.textarea.selectionStart);
@@ -275,12 +294,16 @@ export class AutoComplete {
this.name = this.name.slice(0, this.textarea.selectionStart - (this.parserResult.start) - (this.startQuote ? 1 : 0));
this.parserResult.name = this.name;
this.isReplaceable = true;
this.isForceHidden = false;
this.canBeAutoHidden = false;
} else {
this.isReplaceable = false;
this.canBeAutoHidden = this.basicAutoHideCheck();
}
} else {
// if not forced and no user input -> just show details
this.isReplaceable = false;
this.canBeAutoHidden = this.basicAutoHideCheck();
}
if (isForced || isInput || isSelect) {
@@ -292,8 +315,11 @@ export class AutoComplete {
this.secondaryParserResult = result;
this.name = this.secondaryParserResult.name;
this.isReplaceable = isForced || this.secondaryParserResult.isRequired;
this.isForceHidden = false;
this.canBeAutoHidden = false;
} else {
this.isReplaceable = false;
this.canBeAutoHidden = this.basicAutoHideCheck();
}
}
}
@@ -314,7 +340,17 @@ export class AutoComplete {
// filter the list of options by the partial name according to the matching type
.filter(it => this.isReplaceable || it.name == '' ? matchers[this.matchType](it.name) : it.name.toLowerCase() == this.name)
// remove aliases
.filter((it,idx,list) => list.findIndex(opt=>opt.value == it.value) == idx)
.filter((it,idx,list) => list.findIndex(opt=>opt.value == it.value) == idx);
if (this.result.length == 0 && this.effectiveParserResult != this.parserResult && isForced) {
// no matching secondary results and forced trigger -> show current command details
this.secondaryParserResult = null;
this.result = [this.effectiveParserResult.optionList.find(it=>it.name == this.effectiveParserResult.name)];
this.name = this.effectiveParserResult.name;
this.fuzzyRegex = /(.*)(.*)(.*)/;
}
this.result = this.result
// update remaining options
.map(option => {
// build element
@@ -336,6 +372,15 @@ export class AutoComplete {
;
if (this.isForceHidden) {
// hidden with escape
return this.hide();
}
if (this.autoHide && this.canBeAutoHidden && !isForced && this.effectiveParserResult == this.parserResult && this.result.length == 1) {
// auto hide user setting enabled and somewhere after name part and would usually show command details
return this.hide();
}
if (this.result.length == 0) {
if (!isInput) {
// no result and no input? hide autocomplete
@@ -683,6 +728,8 @@ export class AutoComplete {
if (evt.ctrlKey || evt.altKey || evt.shiftKey) return;
evt.preventDefault();
evt.stopPropagation();
this.isForceHidden = true;
this.wasForced = false;
this.hide();
return;
}

View File

@@ -109,7 +109,7 @@ function downloadAssetsList(url) {
</div>`);
}
for (const i in availableAssets[assetType]) {
for (const i in availableAssets[assetType].sort((a, b) => a?.name && b?.name && a['name'].localeCompare(b['name']))) {
const asset = availableAssets[assetType][i];
const elemId = `assets_install_${assetType}_${i}`;
let element = $('<div />', { id: elemId, class: 'asset-download-button right_menu_button' });
@@ -200,6 +200,9 @@ function downloadAssetsList(url) {
</div>`);
if (assetType === 'character') {
if (asset.highlight) {
assetBlock.find('.asset-name').append('<i class="fa-solid fa-sm fa-trophy"></i>');
}
assetBlock.find('.asset-name').prepend(`<div class="avatar"><img src="${asset['url']}" alt="${displayName}"></div>`);
}

View File

@@ -435,6 +435,7 @@ jQuery(function () {
<select id="caption_multimodal_model" class="flex1 text_pole">
<option data-type="openai" value="gpt-4-vision-preview">gpt-4-vision-preview</option>
<option data-type="openai" value="gpt-4-turbo">gpt-4-turbo</option>
<option data-type="openai" value="gpt-4o">gpt-4o</option>
<option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option>
<option data-type="anthropic" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>
<option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>

View File

@@ -804,10 +804,7 @@ function setMemoryContext(value, saveToMessage, index = null) {
const context = getContext();
context.setExtensionPrompt(MODULE_NAME, formatMemoryValue(value), extension_settings.memory.position, extension_settings.memory.depth, false, extension_settings.memory.role);
$('#memory_contents').val(value);
console.log('Summary set to: ' + value);
console.debug('Position: ' + extension_settings.memory.position);
console.debug('Depth: ' + extension_settings.memory.depth);
console.debug('Role: ' + extension_settings.memory.role);
console.log('Summary set to: ' + value, 'Position: ' + extension_settings.memory.position, 'Depth: ' + extension_settings.memory.depth, 'Role: ' + extension_settings.memory.role);
if (saveToMessage && context.chat.length) {
const idx = index ?? context.chat.length - 2;

View File

@@ -1948,7 +1948,7 @@ async function generatePicture(args, trigger, message, callback) {
}
if (!isValidState()) {
toastr.warning('Extensions API is not connected or doesn\'t provide SD module. Enable Stable Horde to generate images.');
toastr.warning('Image generation is not available. Check your settings and try again.');
return;
}

View File

@@ -642,9 +642,9 @@ jQuery(() => {
loadSettings();
eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, handleIncomingMessage);
eventSource.makeFirst(event_types.CHARACTER_MESSAGE_RENDERED, handleIncomingMessage);
eventSource.makeFirst(event_types.USER_MESSAGE_RENDERED, handleOutgoingMessage);
eventSource.on(event_types.MESSAGE_SWIPED, handleIncomingMessage);
eventSource.on(event_types.USER_MESSAGE_RENDERED, handleOutgoingMessage);
eventSource.on(event_types.IMPERSONATE_READY, handleImpersonateReady);
eventSource.on(event_types.MESSAGE_EDITED, handleMessageEdit);

View File

@@ -1063,8 +1063,8 @@ $(document).ready(function () {
eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
eventSource.on(event_types.MESSAGE_DELETED, onMessageDeleted);
eventSource.on(event_types.GROUP_UPDATED, onChatChanged);
eventSource.on(event_types.MESSAGE_SENT, onMessageEvent);
eventSource.on(event_types.MESSAGE_RECEIVED, onMessageEvent);
eventSource.makeLast(event_types.CHARACTER_MESSAGE_RENDERED, onMessageEvent);
eventSource.makeLast(event_types.USER_MESSAGE_RENDERED, onMessageEvent);
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'speak',
callback: onNarrateText,
aliases: ['narrate', 'tts'],

View File

@@ -5,7 +5,9 @@ const storageKey = 'language';
const overrideLanguage = localStorage.getItem(storageKey);
const localeFile = String(overrideLanguage || navigator.language || navigator.userLanguage || 'en').toLowerCase();
const langs = await fetch('/locales/lang.json').then(response => response.json());
const localeData = await getLocaleData(localeFile);
// Don't change to let/const! It will break module loading.
// eslint-disable-next-line prefer-const
var localeData = await getLocaleData(localeFile);
/**
* Fetches the locale data for the given language.

View File

@@ -50,6 +50,7 @@ import {
download,
getBase64Async,
getFileText,
getImageSizeFromDataURL,
getSortableDelay,
isDataURL,
parseJsonFile,
@@ -273,6 +274,7 @@ const default_settings = {
use_alt_scale: false,
squash_system_messages: false,
image_inlining: false,
inline_image_quality: 'low',
bypass_status_check: false,
continue_prefill: false,
names_behavior: character_names_behavior.NONE,
@@ -348,6 +350,7 @@ const oai_settings = {
use_alt_scale: false,
squash_system_messages: false,
image_inlining: false,
inline_image_quality: 'low',
bypass_status_check: false,
continue_prefill: false,
names_behavior: character_names_behavior.NONE,
@@ -2188,12 +2191,47 @@ class Message {
}
}
const quality = oai_settings.inline_image_quality || default_settings.inline_image_quality;
this.content = [
{ type: 'text', text: textContent },
{ type: 'image_url', image_url: { 'url': image, 'detail': 'low' } },
{ type: 'image_url', image_url: { 'url': image, 'detail': quality } },
];
this.tokens += Message.tokensPerImage;
const tokens = await this.getImageTokenCost(image, quality);
this.tokens += tokens;
}
async getImageTokenCost(dataUrl, quality) {
if (quality === 'low') {
return Message.tokensPerImage;
}
const size = await getImageSizeFromDataURL(dataUrl);
// If the image is small enough, we can use the low quality token cost
if (quality === 'auto' && size.width <= 512 && size.height <= 512) {
return Message.tokensPerImage;
}
/*
* Images are first scaled to fit within a 2048 x 2048 square, maintaining their aspect ratio.
* Then, they are scaled such that the shortest side of the image is 768px long.
* Finally, we count how many 512px squares the image consists of.
* Each of those squares costs 170 tokens. Another 85 tokens are always added to the final total.
* https://platform.openai.com/docs/guides/vision/calculating-costs
*/
const scale = 2048 / Math.min(size.width, size.height);
const scaledWidth = Math.round(size.width * scale);
const scaledHeight = Math.round(size.height * scale);
const finalScale = 768 / Math.min(scaledWidth, scaledHeight);
const finalWidth = Math.round(scaledWidth * finalScale);
const finalHeight = Math.round(scaledHeight * finalScale);
const squares = Math.ceil(finalWidth / 512) * Math.ceil(finalHeight / 512);
const tokens = squares * 170 + 85;
return tokens;
}
/**
@@ -2722,6 +2760,7 @@ function loadOpenAISettings(data, settings) {
oai_settings.assistant_prefill = settings.assistant_prefill ?? default_settings.assistant_prefill;
oai_settings.human_sysprompt_message = settings.human_sysprompt_message ?? default_settings.human_sysprompt_message;
oai_settings.image_inlining = settings.image_inlining ?? default_settings.image_inlining;
oai_settings.inline_image_quality = settings.inline_image_quality ?? default_settings.inline_image_quality;
oai_settings.bypass_status_check = settings.bypass_status_check ?? default_settings.bypass_status_check;
oai_settings.seed = settings.seed ?? default_settings.seed;
oai_settings.n = settings.n ?? default_settings.n;
@@ -2759,6 +2798,9 @@ function loadOpenAISettings(data, settings) {
$('#openai_image_inlining').prop('checked', oai_settings.image_inlining);
$('#openai_bypass_status_check').prop('checked', oai_settings.bypass_status_check);
$('#openai_inline_image_quality').val(oai_settings.inline_image_quality);
$(`#openai_inline_image_quality option[value="${oai_settings.inline_image_quality}"]`).prop('selected', true);
$('#model_openai_select').val(oai_settings.openai_model);
$(`#model_openai_select option[value="${oai_settings.openai_model}"`).attr('selected', true);
$('#model_claude_select').val(oai_settings.claude_model);
@@ -3079,6 +3121,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
use_alt_scale: settings.use_alt_scale,
squash_system_messages: settings.squash_system_messages,
image_inlining: settings.image_inlining,
inline_image_quality: settings.inline_image_quality,
bypass_status_check: settings.bypass_status_check,
continue_prefill: settings.continue_prefill,
continue_postfix: settings.continue_postfix,
@@ -3464,6 +3507,7 @@ function onSettingsPresetChange() {
use_alt_scale: ['#use_alt_scale', 'use_alt_scale', true],
squash_system_messages: ['#squash_system_messages', 'squash_system_messages', true],
image_inlining: ['#openai_image_inlining', 'image_inlining', true],
inline_image_quality: ['#openai_inline_image_quality', 'inline_image_quality', false],
continue_prefill: ['#continue_prefill', 'continue_prefill', true],
continue_postfix: ['#continue_postfix', 'continue_postfix', false],
seed: ['#seed_openai', 'seed', false],
@@ -3515,7 +3559,7 @@ function getMaxContextOpenAI(value) {
if (oai_settings.max_context_unlocked) {
return unlocked_max;
}
else if (value.includes('gpt-4-turbo') || value.includes('gpt-4-1106') || value.includes('gpt-4-0125') || value.includes('gpt-4-vision')) {
else if (value.includes('gpt-4-turbo') || value.includes('gpt-4o') || value.includes('gpt-4-1106') || value.includes('gpt-4-0125') || value.includes('gpt-4-vision')) {
return max_128k;
}
else if (value.includes('gpt-3.5-turbo-1106')) {
@@ -4244,6 +4288,7 @@ export function isImageInliningSupported() {
'gemini-pro-vision',
'claude-3',
'gpt-4-turbo',
'gpt-4o',
];
switch (oai_settings.chat_completion_source) {
@@ -4707,6 +4752,11 @@ $(document).ready(async function () {
saveSettingsDebounced();
});
$('#openai_inline_image_quality').on('input', function () {
oai_settings.inline_image_quality = String($(this).val());
saveSettingsDebounced();
});
$('#continue_prefill').on('input', function () {
oai_settings.continue_prefill = !!$(this).prop('checked');
saveSettingsDebounced();

View File

@@ -259,6 +259,7 @@ let power_user = {
stscript: {
matching: 'fuzzy',
autocomplete: {
autoHide: false,
style: 'theme',
font: {
scale: 0.8,
@@ -1618,6 +1619,7 @@ function loadPowerUserSettings(settings, data) {
$('#token_padding').val(power_user.token_padding);
$('#aux_field').val(power_user.aux_field);
$('#stscript_autocomplete_autoHide').prop('checked', power_user.stscript.autocomplete.autoHide ?? false).trigger('input');
$('#stscript_matching').val(power_user.stscript.matching ?? 'fuzzy');
$('#stscript_autocomplete_style').val(power_user.stscript.autocomplete_style ?? 'theme');
document.body.setAttribute('data-stscript-style', power_user.stscript.autocomplete_style);
@@ -3646,6 +3648,11 @@ $(document).ready(() => {
saveSettingsDebounced();
});
$('#stscript_autocomplete_autoHide').on('input', function () {
power_user.stscript.autocomplete.autoHide = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#stscript_matching').on('change', function () {
const value = $(this).find(':selected').val();
power_user.stscript.matching = String(value);

View File

@@ -847,6 +847,12 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'messages',
new SlashCommandNamedArgument(
'names', 'show message author names', [ARGUMENT_TYPE.BOOLEAN], false, false, 'off', ['off', 'on'],
),
new SlashCommandNamedArgument(
'hidden', 'include hidden messages', [ARGUMENT_TYPE.BOOLEAN], false, false, 'on', ['off', 'on'],
),
new SlashCommandNamedArgument(
'role', 'filter messages by role' , [ARGUMENT_TYPE.STRING], false, false, null, ['system', 'assistant', 'user'],
),
],
unnamedArgumentList: [
new SlashCommandArgument(
@@ -858,6 +864,12 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'messages',
<div>
Returns the specified message or range of messages as a string.
</div>
<div>
Use the <code>hidden=off</code> argument to exclude hidden messages.
</div>
<div>
Use the <code>role</code> argument to filter messages by role. Possible values are: system, assistant, user.
</div>
<div>
<strong>Examples:</strong>
<ul>
@@ -1310,13 +1322,37 @@ async function popupCallback(args, value) {
function getMessagesCallback(args, value) {
const includeNames = !isFalseBoolean(args?.names);
const includeHidden = isTrueBoolean(args?.hidden);
const role = args?.role;
const range = stringToRange(value, 0, chat.length - 1);
if (!range) {
console.warn(`WARN: Invalid range provided for /getmessages command: ${value}`);
console.warn(`WARN: Invalid range provided for /messages command: ${value}`);
return '';
}
const filterByRole = (mes) => {
if (!role) {
return true;
}
const isNarrator = mes.extra?.type === system_message_types.NARRATOR;
if (role === 'system') {
return isNarrator && !mes.is_user;
}
if (role === 'assistant') {
return !isNarrator && !mes.is_user;
}
if (role === 'user') {
return !isNarrator && mes.is_user;
}
throw new Error(`Invalid role provided. Expected one of: system, assistant, user. Got: ${role}`);
};
const messages = [];
for (let messageId = range.start; messageId <= range.end; messageId++) {
@@ -1326,7 +1362,13 @@ function getMessagesCallback(args, value) {
continue;
}
if (message.is_system) {
if (role && !filterByRole(message)) {
console.debug(`/messages: Skipping message with ID ${messageId} due to role filter`);
continue;
}
if (!includeHidden && message.is_system) {
console.debug(`/messages: Skipping hidden message with ID ${messageId}`);
continue;
}

View File

@@ -99,7 +99,7 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
const result = new AutoCompleteSecondaryNameResult(
value,
start + name.length,
cmdArg.enumList.map(it=>new SlashCommandEnumAutoCompleteOption(it)),
cmdArg.enumList.map(it=>new SlashCommandEnumAutoCompleteOption(this.executor.command, it)),
true,
);
result.isRequired = true;
@@ -154,7 +154,7 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
const result = new AutoCompleteSecondaryNameResult(
value,
start,
cmdArg.enumList.map(it=>new SlashCommandEnumAutoCompleteOption(it)),
cmdArg.enumList.map(it=>new SlashCommandEnumAutoCompleteOption(this.executor.command, it)),
false,
);
const isCompleteValue = cmdArg.enumList.find(it=>it.value == value);

View File

@@ -1,16 +1,20 @@
import { AutoCompleteOption } from '../autocomplete/AutoCompleteOption.js';
import { SlashCommand } from './SlashCommand.js';
import { SlashCommandEnumValue } from './SlashCommandEnumValue.js';
export class SlashCommandEnumAutoCompleteOption extends AutoCompleteOption {
/**@type {SlashCommand}*/ cmd;
/**@type {SlashCommandEnumValue}*/ enumValue;
/**
* @param {SlashCommand} cmd
* @param {SlashCommandEnumValue} enumValue
*/
constructor(enumValue) {
constructor(cmd, enumValue) {
super(enumValue.value, '◊');
this.cmd = cmd;
this.enumValue = enumValue;
}
@@ -25,22 +29,6 @@ export class SlashCommandEnumAutoCompleteOption extends AutoCompleteOption {
renderDetails() {
const frag = document.createDocumentFragment();
const specs = document.createElement('div'); {
specs.classList.add('specs');
const name = document.createElement('div'); {
name.classList.add('name');
name.classList.add('monospace');
name.textContent = this.name;
specs.append(name);
}
frag.append(specs);
}
const help = document.createElement('span'); {
help.classList.add('help');
help.textContent = this.enumValue.description;
frag.append(help);
}
return frag;
return this.cmd.renderHelpDetails();
}
}

View File

@@ -27,22 +27,6 @@ export class SlashCommandNamedArgumentAutoCompleteOption extends AutoCompleteOpt
renderDetails() {
const frag = document.createDocumentFragment();
const specs = document.createElement('div'); {
specs.classList.add('specs');
const name = document.createElement('div'); {
name.classList.add('name');
name.classList.add('monospace');
name.textContent = this.name;
specs.append(name);
}
frag.append(specs);
}
const help = document.createElement('span'); {
help.classList.add('help');
help.innerHTML = `${this.arg.isRequired ? '' : '(optional) '}${this.arg.description ?? ''}`;
frag.append(help);
}
return frag;
return this.cmd.renderHelpDetails();
}
}

View File

@@ -362,7 +362,6 @@ export class SlashCommandParser {
;
if (childClosure !== null) return null;
const macro = this.macroIndex.findLast(it=>it.start <= index && it.end >= index);
console.log(macro);
if (macro) {
const frag = document.createRange().createContextualFragment(await (await fetch('/scripts/templates/macros.html')).text());
const options = [...frag.querySelectorAll('ul:nth-of-type(2n+1) > li')].map(li=>new MacroAutoCompleteOption(

View File

@@ -732,6 +732,24 @@ export function isDataURL(str) {
return regex.test(str);
}
/**
* Gets the size of an image from a data URL.
* @param {string} dataUrl Image data URL
* @returns {Promise<{ width: number, height: number }>} Image size
*/
export function getImageSizeFromDataURL(dataUrl) {
const image = new Image();
image.src = dataUrl;
return new Promise((resolve, reject) => {
image.onload = function () {
resolve({ width: image.width, height: image.height });
};
image.onerror = function () {
reject(new Error('Failed to load image'));
};
});
}
export function getCharaFilename(chid) {
const context = getContext();
const fileName = context.characters[chid ?? context.characterId].avatar;

View File

@@ -1036,7 +1036,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
$('#world_info_pagination').pagination({
dataSource: getDataArray,
pageSize: Number(localStorage.getItem(storageKey)) || perPageDefault,
sizeChangerOptions: [10, 25, 50, 100],
sizeChangerOptions: [10, 25, 50, 100, 500, 1000],
showSizeChanger: true,
pageRange: 1,
pageNumber: startPage,
@@ -2888,8 +2888,8 @@ function convertAgnaiMemoryBook(inputObj) {
addMemo: !!entry.name,
excludeRecursion: false,
displayIndex: index,
probability: null,
useProbability: false,
probability: 100,
useProbability: true,
group: '',
groupOverride: false,
groupWeight: DEFAULT_WEIGHT,
@@ -2926,8 +2926,8 @@ function convertRisuLorebook(inputObj) {
addMemo: true,
excludeRecursion: false,
displayIndex: index,
probability: entry.activationPercent ?? null,
useProbability: entry.activationPercent ?? false,
probability: entry.activationPercent ?? 100,
useProbability: entry.activationPercent ?? true,
group: '',
groupOverride: false,
groupWeight: DEFAULT_WEIGHT,
@@ -2969,8 +2969,8 @@ function convertNovelLorebook(inputObj) {
addMemo: addMemo,
excludeRecursion: false,
displayIndex: index,
probability: null,
useProbability: false,
probability: 100,
useProbability: true,
group: '',
groupOverride: false,
groupWeight: DEFAULT_WEIGHT,
@@ -3011,8 +3011,8 @@ function convertCharacterBook(characterBook) {
disable: !entry.enabled,
addMemo: entry.comment ? true : false,
displayIndex: entry.extensions?.display_index ?? index,
probability: entry.extensions?.probability ?? null,
useProbability: entry.extensions?.useProbability ?? false,
probability: entry.extensions?.probability ?? 100,
useProbability: entry.extensions?.useProbability ?? true,
depth: entry.extensions?.depth ?? DEFAULT_DEPTH,
selectiveLogic: entry.extensions?.selectiveLogic ?? world_info_logic.AND_ANY,
group: entry.extensions?.group ?? '',

View File

@@ -1112,8 +1112,8 @@ select {
}
#send_textarea {
min-height: calc(var(--bottomFormBlockSize) + 3px);
height: calc(var(--bottomFormBlockSize) + 3px);
min-height: calc(var(--bottomFormBlockSize) + 2px);
height: calc(var(--bottomFormBlockSize) + 2px);
max-height: 50vh;
max-height: 50svh;
word-wrap: break-word;