diff --git a/package.json b/package.json index 278b63283..ba6fb92af 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/plugins.js b/plugins.js index 4615951c6..63d21778a 100644 --- a/plugins.js +++ b/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); + } +} diff --git a/public/index.html b/public/index.html index c2f391e53..907f67836 100644 --- a/public/index.html +++ b/public/index.html @@ -1680,7 +1680,7 @@
-

AutoComplete Settings

+
@@ -4171,14 +4191,14 @@ diff --git a/public/lib/eventemitter.js b/public/lib/eventemitter.js index a1b40e38c..3177518de 100644 --- a/public/lib/eventemitter.js +++ b/public/lib/eventemitter.js @@ -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; diff --git a/public/locales/ko-kr.json b/public/locales/ko-kr.json index 03fb8c8f1..1bda6fb97 100644 --- a/public/locales/ko-kr.json +++ b/public/locales/ko-kr.json @@ -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": "로딩할 메시지 수" } \ No newline at end of file diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js index 5239b71c3..826f968ea 100644 --- a/public/scripts/RossAscends-mods.js +++ b/public/scripts/RossAscends-mods.js @@ -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; diff --git a/public/scripts/autocomplete/AutoComplete.js b/public/scripts/autocomplete/AutoComplete.js index d1205f6e8..dc000f732 100644 --- a/public/scripts/autocomplete/AutoComplete.js +++ b/public/scripts/autocomplete/AutoComplete.js @@ -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; } diff --git a/public/scripts/extensions/assets/index.js b/public/scripts/extensions/assets/index.js index 50bad2479..2d96fa875 100644 --- a/public/scripts/extensions/assets/index.js +++ b/public/scripts/extensions/assets/index.js @@ -109,7 +109,7 @@ function downloadAssetsList(url) {
`); } - 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 = $('
', { id: elemId, class: 'asset-download-button right_menu_button' }); @@ -200,6 +200,9 @@ function downloadAssetsList(url) {
`); if (assetType === 'character') { + if (asset.highlight) { + assetBlock.find('.asset-name').append(''); + } assetBlock.find('.asset-name').prepend(`
${displayName}
`); } diff --git a/public/scripts/extensions/caption/index.js b/public/scripts/extensions/caption/index.js index 98ac565a2..f0738e5d5 100644 --- a/public/scripts/extensions/caption/index.js +++ b/public/scripts/extensions/caption/index.js @@ -435,6 +435,7 @@ jQuery(function () {