mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Merge branch 'SillyTavern:staging' into staging
This commit is contained in:
@@ -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"
|
||||
|
22
plugins.js
22
plugins.js
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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": "로딩할 메시지 수"
|
||||
|
||||
}
|
@@ -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;
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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>`);
|
||||
}
|
||||
|
||||
|
@@ -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>
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -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'],
|
||||
|
@@ -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.
|
||||
|
@@ -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();
|
||||
|
@@ -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);
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
@@ -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(
|
||||
|
@@ -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;
|
||||
|
@@ -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 ?? '',
|
||||
|
@@ -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;
|
||||
|
Reference in New Issue
Block a user