diff --git a/public/css/st-tailwind.css b/public/css/st-tailwind.css index aa21daaee..f2c1c55e6 100644 --- a/public/css/st-tailwind.css +++ b/public/css/st-tailwind.css @@ -220,7 +220,7 @@ } .monospace { - font-family: monospace; + font-family: var(--monoFontFamily); } .expander { diff --git a/public/index.html b/public/index.html index e9a840353..9f43cd7ac 100644 --- a/public/index.html +++ b/public/index.html @@ -18,6 +18,7 @@ + diff --git a/public/login.html b/public/login.html index ff390be68..c52ca3d74 100644 --- a/public/login.html +++ b/public/login.html @@ -26,6 +26,7 @@ + diff --git a/public/script.js b/public/script.js index 38f069968..ebd67cbff 100644 --- a/public/script.js +++ b/public/script.js @@ -764,8 +764,19 @@ const per_page_default = 50; var is_advanced_char_open = false; -export let menu_type = ''; //what is selected in the menu +/** + * The type of the right menu + * @typedef {'characters' | 'character_edit' | 'create' | 'group_edit' | 'group_create' | '' } MenuType + */ + +/** + * The type of the right menu that is currently open + * @type {MenuType} + */ +export let menu_type = ''; + export let selected_button = ''; //which button pressed + //create pole save let create_save = { name: '', @@ -5432,8 +5443,14 @@ export function resetChatState() { characters.length = 0; } +/** + * + * @param {'characters' | 'character_edit' | 'create' | 'group_edit' | 'group_create'} value + */ export function setMenuType(value) { menu_type = value; + // Allow custom CSS to see which menu type is active + document.getElementById('right-nav-panel').dataset.menuType = menu_type; } export function setExternalAbortController(controller) { @@ -6815,7 +6832,7 @@ export function select_selected_character(chid) { //character select //console.log('select_selected_character() -- starting with input of -- ' + chid + ' (name:' + characters[chid].name + ')'); select_rm_create(); - menu_type = 'character_edit'; + setMenuType('character_edit'); $('#delete_button').css('display', 'flex'); $('#export_button').css('display', 'flex'); var display_name = characters[chid].name; @@ -6891,7 +6908,7 @@ export function select_selected_character(chid) { } function select_rm_create() { - menu_type = 'create'; + setMenuType('create'); //console.log('select_rm_Create() -- selected button: '+selected_button); if (selected_button == 'create') { @@ -6952,7 +6969,7 @@ function select_rm_create() { function select_rm_characters() { const doFullRefresh = menu_type === 'characters'; - menu_type = 'characters'; + setMenuType('characters'); selectRightMenuWithAnimation('rm_characters_block'); printCharacters(doFullRefresh); } @@ -8978,7 +8995,6 @@ jQuery(async function () { $('#rm_button_settings').click(function () { selected_button = 'settings'; - menu_type = 'settings'; selectRightMenuWithAnimation('rm_api_block'); }); $('#rm_button_characters').click(function () { diff --git a/public/scripts/PromptManager.js b/public/scripts/PromptManager.js index 806b5f308..dbf7dd4ed 100644 --- a/public/scripts/PromptManager.js +++ b/public/scripts/PromptManager.js @@ -685,6 +685,23 @@ class PromptManager { this.log('Initialized'); } + /** + * Get the scroll position of the prompt manager + * @returns {number} - Scroll position of the prompt manager + */ + #getScrollPosition() { + return document.getElementById(this.configuration.prefix + 'prompt_manager')?.closest('.scrollableInner')?.scrollTop; + } + + /** + * Set the scroll position of the prompt manager + * @param {number} scrollPosition - The scroll position to set + */ + #setScrollPosition(scrollPosition) { + if (scrollPosition === undefined || scrollPosition === null) return; + document.getElementById(this.configuration.prefix + 'prompt_manager')?.closest('.scrollableInner')?.scrollTo(0, scrollPosition); + } + /** * Main rendering function * @@ -703,17 +720,21 @@ class PromptManager { this.tryGenerate().finally(async () => { this.profileEnd('filling context'); this.profileStart('render'); + const scrollPosition = this.#getScrollPosition(); await this.renderPromptManager(); await this.renderPromptManagerListItems(); this.makeDraggable(); + this.#setScrollPosition(scrollPosition); this.profileEnd('render'); }); } else { // Executed during live communication this.profileStart('render'); + const scrollPosition = this.#getScrollPosition(); await this.renderPromptManager(); await this.renderPromptManagerListItems(); this.makeDraggable(); + this.#setScrollPosition(scrollPosition); this.profileEnd('render'); } }).catch(() => { diff --git a/public/scripts/autocomplete/AutoComplete.js b/public/scripts/autocomplete/AutoComplete.js index 2d5818e22..5f1dd32a9 100644 --- a/public/scripts/autocomplete/AutoComplete.js +++ b/public/scripts/autocomplete/AutoComplete.js @@ -482,8 +482,8 @@ export class AutoComplete { this.domWrap.style.setProperty('--leftOffset', `max(1vw, ${rect[power_user.stscript.autocomplete.width.left].left}px)`); this.domWrap.style.setProperty('--rightOffset', `calc(100vw - min(99vw, ${rect[power_user.stscript.autocomplete.width.right].right}px)`); } - this.updateDetailsPosition(); } + this.updateDetailsPosition(); } /** diff --git a/public/scripts/extensions/quick-reply/html/qrEditor.html b/public/scripts/extensions/quick-reply/html/qrEditor.html index 40b4e8db5..f6d95ead5 100644 --- a/public/scripts/extensions/quick-reply/html/qrEditor.html +++ b/public/scripts/extensions/quick-reply/html/qrEditor.html @@ -29,10 +29,14 @@ Ctrl+Enter to execute +
- +
diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index 33b36e6fb..f4a09906f 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -264,6 +264,13 @@ export class QuickReply { const updateSyntax = ()=>{ messageSyntaxInner.innerHTML = hljs.highlight(`${message.value}${message.value.slice(-1) == '\n' ? ' ' : ''}`, { language:'stscript', ignoreIllegals:true })?.value; }; + const updateSyntaxEnabled = ()=>{ + if (JSON.parse(localStorage.getItem('qr--syntax'))) { + dom.querySelector('#qr--modal-messageHolder').classList.remove('qr--noSyntax'); + } else { + dom.querySelector('#qr--modal-messageHolder').classList.add('qr--noSyntax'); + } + }; /**@type {HTMLInputElement}*/ const tabSize = dom.querySelector('#qr--modal-tabSize'); tabSize.value = JSON.parse(localStorage.getItem('qr--tabSize') ?? '4'); @@ -282,6 +289,13 @@ export class QuickReply { executeShortcut.addEventListener('click', () => { localStorage.setItem('qr--executeShortcut', JSON.stringify(executeShortcut.checked)); }); + /**@type {HTMLInputElement}*/ + const syntax = dom.querySelector('#qr--modal-syntax'); + syntax.checked = JSON.parse(localStorage.getItem('qr--syntax') ?? 'true'); + syntax.addEventListener('click', () => { + localStorage.setItem('qr--syntax', JSON.stringify(syntax.checked)); + updateSyntaxEnabled(); + }); /**@type {HTMLTextAreaElement}*/ const message = dom.querySelector('#qr--modal-message'); message.value = this.message; @@ -352,8 +366,7 @@ export class QuickReply { } }); window.addEventListener('resize', resizeListener); - message.style.color = 'transparent'; - message.style.background = 'transparent'; + updateSyntaxEnabled(); message.style.setProperty('text-shadow', 'none', 'important'); /**@type {HTMLElement}*/ const messageSyntaxInner = dom.querySelector('#qr--modal-messageSyntaxInner'); diff --git a/public/scripts/extensions/quick-reply/style.css b/public/scripts/extensions/quick-reply/style.css index b7b8d8ffc..dfab049d8 100644 --- a/public/scripts/extensions/quick-reply/style.css +++ b/public/scripts/extensions/quick-reply/style.css @@ -301,6 +301,22 @@ text-align: left; overflow: hidden; } +.dialogue_popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-messageSyntax { + display: none; +} +.dialogue_popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message { + background-color: var(--ac-style-color-background); + color: var(--ac-style-color-text); +} +.dialogue_popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::selection { + color: unset; + background-color: rgba(108 171 251 / 0.25); +} +@supports (color: rgb(from white r g b / 0.25)) { + .dialogue_popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::selection { + background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25); + } +} .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-messageSyntax { grid-column: 1; grid-row: 1; @@ -315,18 +331,30 @@ height: 100%; } .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message { + background-color: transparent; + color: transparent; grid-column: 1; grid-row: 1; - caret-color: white; - mix-blend-mode: difference; + caret-color: var(--ac-style-color-text); + overflow: auto; } .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message::-webkit-scrollbar, .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message::-webkit-scrollbar-thumb { visibility: hidden; cursor: default; } +.dialogue_popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message::selection { + color: transparent; + background-color: rgba(108 171 251 / 0.25); +} +@supports (color: rgb(from white r g b / 0.25)) { + .dialogue_popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message::selection { + background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25); + } +} .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder #qr--modal-message, .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder #qr--modal-messageSyntaxInner { + font-family: var(--monoFontFamily); padding: 0.75em; margin: 0; border: none; diff --git a/public/scripts/extensions/quick-reply/style.less b/public/scripts/extensions/quick-reply/style.less index 5de370a4f..0d6b92a04 100644 --- a/public/scripts/extensions/quick-reply/style.less +++ b/public/scripts/extensions/quick-reply/style.less @@ -322,6 +322,22 @@ display: grid; text-align: left; overflow: hidden; + &.qr--noSyntax { + > #qr--modal-messageSyntax { + display: none; + } + > #qr--modal-message { + background-color: var(--ac-style-color-background); + color: var(--ac-style-color-text); + &::selection { + color: unset; + background-color: rgba(108 171 251 / 0.25); + @supports (color: rgb(from white r g b / 0.25)) { + background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25); + } + } + } + } > #qr--modal-messageSyntax { grid-column: 1; grid-row: 1; @@ -336,16 +352,26 @@ } } > #qr--modal-message { + background-color: transparent; + color: transparent; grid-column: 1; grid-row: 1; - caret-color: white; - mix-blend-mode: difference; + caret-color: var(--ac-style-color-text); + overflow: auto; &::-webkit-scrollbar, &::-webkit-scrollbar-thumb { visibility: hidden; cursor: default; } + &::selection { + color: transparent; + background-color: rgba(108 171 251 / 0.25); + @supports (color: rgb(from white r g b / 0.25)) { + background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25); + } + } } #qr--modal-message, #qr--modal-messageSyntaxInner { + font-family: var(--monoFontFamily); padding: 0.75em; margin: 0; border: none; diff --git a/public/scripts/extensions/translate/index.js b/public/scripts/extensions/translate/index.js index 20519db5b..36589ca3a 100644 --- a/public/scripts/extensions/translate/index.js +++ b/public/scripts/extensions/translate/index.js @@ -16,6 +16,7 @@ import { SlashCommand } from '../../slash-commands/SlashCommand.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; import { splitRecursive } from '../../utils.js'; +import { renderTemplateAsync } from '../../templates.js'; export const autoModeOptions = { NONE: 'none', @@ -340,6 +341,34 @@ async function translateProviderBing(text, lang) { throw new Error(response.statusText); } +/** + * Translates text using the Yandex Translate API + * @param {string} text Text to translate + * @param {string} lang Target language code + * @returns {Promise} Translated text + */ +async function translateProviderYandex(text, lang) { + let chunks = []; + const chunkSize = 5000; + if (text.length <= chunkSize) { + chunks.push(text); + } else { + chunks = splitRecursive(text, chunkSize); + } + const response = await fetch('/api/translate/yandex', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ chunks: chunks, lang: lang }), + }); + + if (response.ok) { + const result = await response.text(); + return result; + } + + throw new Error(response.statusText); +} + /** * Splits text into chunks and translates each chunk separately * @param {string} text Text to translate @@ -389,6 +418,8 @@ async function translate(text, lang) { return await translateProviderOneRing(text, lang); case 'bing': return await chunkedTranslate(text, lang, translateProviderBing, 1000); + case 'yandex': + return await translateProviderYandex(text, lang); default: console.error('Unknown translation provider', extension_settings.translate.provider); return text; @@ -532,56 +563,10 @@ const handleMessageEdit = createEventHandler(translateMessageEdit, () => true); window['translate'] = translate; -jQuery(() => { - const html = ` -
-
-
- Chat Translation -
-
-
- - - -
- - - -
- - - -
-
-
`; +jQuery(async() => { + const html = await renderTemplateAsync('translateIndex'); - const buttonHtml = ` -
-
- Translate Chat -
-
-
- Translate Input -
- `; + const buttonHtml = await renderTemplateAsync('translateButtons'); $('#extensionsMenu').append(buttonHtml); $('#extensions_settings2').append(html); $('#translate_chat').on('click', onTranslateChatClick); @@ -624,7 +609,7 @@ jQuery(() => { 'libre': 'http://127.0.0.1:5000/translate', 'lingva': 'https://lingva.ml/api/v1', 'oneringtranslator': 'http://127.0.0.1:4990/translate', - 'deeplx': 'http://127.0.0.1:1188/translate', + 'deeplx': 'http://127.0.0.1:1188/translate' }; const popupText = `

${optionText} API URL

Example: ${String(exampleURLs[extension_settings.translate.provider])}`; diff --git a/public/scripts/extensions/tts/sbvits2.js b/public/scripts/extensions/tts/sbvits2.js index ab41d4f6d..7f542ca6a 100644 --- a/public/scripts/extensions/tts/sbvits2.js +++ b/public/scripts/extensions/tts/sbvits2.js @@ -19,6 +19,8 @@ class SBVits2TtsProvider { * @returns {string} Processed text */ processText(text) { + // backup for auto_split + text = text.replace(/\n+/g, '
'); return text; } @@ -276,6 +278,8 @@ class SBVits2TtsProvider { const [model_id, speaker_id, style] = voiceId.split('-'); const params = new URLSearchParams(); + // restore for auto_split + inputText = inputText.replaceAll('
', '\n'); params.append('text', inputText); params.append('model_id', model_id); params.append('speaker_id', speaker_id); diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 6e29ea39e..6f7729ce9 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -84,15 +84,21 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ helpString: 'Get help on macros, chat formatting and commands.', })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'name', + name: 'persona', callback: setNameCallback, - unnamedArgumentList: [ - new SlashCommandArgument( - 'persona', [ARGUMENT_TYPE.STRING], true, + namedArgumentList: [ + new SlashCommandNamedArgument( + 'mode', 'The mode for persona selection. ("lookup" = search for existing persona, "temp" = create a temporary name, set a temporary name, "all" = allow both in the same command)', + [ARGUMENT_TYPE.STRING], false, false, 'all', ['lookup', 'temp', 'all'], ), ], - helpString: 'Sets user name and persona avatar (if set).', - aliases: ['persona'], + unnamedArgumentList: [ + new SlashCommandArgument( + 'persona name', [ARGUMENT_TYPE.STRING], true, + ), + ], + helpString: 'Selects the given persona with its name and avatar (by name or avatar url). If no matching persona exists, applies a temporary name.', + aliases: ['name'], })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'sync', @@ -2372,26 +2378,44 @@ function setFlatModeCallback() { $('#chat_display').val(chat_styles.DEFAULT).trigger('change'); } -function setNameCallback(_, name) { +/** + * Sets a persona name and optionally an avatar. + * @param {{mode: 'lookup' | 'temp' | 'all'}} namedArgs Named arguments + * @param {string} name Name to set + * @returns {void} + */ +function setNameCallback({ mode = 'all' }, name) { if (!name) { - toastr.warning('you must specify a name to change to'); + toastr.warning('You must specify a name to change to'); + return; + } + + if (!['lookup', 'temp', 'all'].includes(mode)) { + toastr.warning('Mode must be one of "lookup", "temp" or "all"'); return; } name = name.trim(); - // If the name is a persona, auto-select it - for (let persona of Object.values(power_user.personas)) { - if (persona.toLowerCase() === name.toLowerCase()) { - autoSelectPersona(name); + // If the name matches a persona avatar, or a name, auto-select it + if (['lookup', 'all'].includes(mode)) { + let persona = Object.entries(power_user.personas).find(([avatar, _]) => avatar === name)?.[1]; + if (!persona) persona = Object.entries(power_user.personas).find(([_, personaName]) => personaName.toLowerCase() === name.toLowerCase())?.[1]; + if (persona) { + autoSelectPersona(persona); retriggerFirstMessageOnEmptyChat(); return; + } else if (mode === 'lookup') { + toastr.warning(`Persona ${name} not found`); + return; } } - // Otherwise, set just the name - setUserName(name); //this prevented quickReply usage - retriggerFirstMessageOnEmptyChat(); + if (['temp', 'all'].includes(mode)) { + // Otherwise, set just the name + setUserName(name); //this prevented quickReply usage + retriggerFirstMessageOnEmptyChat(); + } } async function setNarratorName(_, text) { diff --git a/public/scripts/templates/translateButtons.html b/public/scripts/templates/translateButtons.html new file mode 100644 index 000000000..7f1549f40 --- /dev/null +++ b/public/scripts/templates/translateButtons.html @@ -0,0 +1,8 @@ +
+
+ Translate Chat +
+
+
+ Translate Input +
\ No newline at end of file diff --git a/public/scripts/templates/translateIndex.html b/public/scripts/templates/translateIndex.html new file mode 100644 index 000000000..d0f3d5ea4 --- /dev/null +++ b/public/scripts/templates/translateIndex.html @@ -0,0 +1,38 @@ +
+
+
+ Chat Translation +
+
+
+ + + +
+ + + +
+ + + +
+
+
\ No newline at end of file diff --git a/public/scripts/utils.js b/public/scripts/utils.js index 5d5fe0f1d..3a0441e7f 100644 --- a/public/scripts/utils.js +++ b/public/scripts/utils.js @@ -495,14 +495,16 @@ export function trimToEndSentence(input, include_newline = false) { return ''; } + const isEmoji = x => /(\p{Emoji_Presentation}|\p{Extended_Pictographic})/gu.test(x); const punctuation = new Set(['.', '!', '?', '*', '"', ')', '}', '`', ']', '$', '。', '!', '?', '”', ')', '】', '’', '」', '_']); // extend this as you see fit let last = -1; - for (let i = input.length - 1; i >= 0; i--) { - const char = input[i]; + const characters = Array.from(input); + for (let i = characters.length - 1; i >= 0; i--) { + const char = characters[i]; - if (punctuation.has(char)) { - if (i > 0 && /[\s\n]/.test(input[i - 1])) { + if (punctuation.has(char) || isEmoji(char)) { + if (i > 0 && /[\s\n]/.test(characters[i - 1])) { last = i - 1; } else { last = i; @@ -520,7 +522,7 @@ export function trimToEndSentence(input, include_newline = false) { return input.trimEnd(); } - return input.substring(0, last + 1).trimEnd(); + return characters.slice(0, last + 1).join('').trimEnd(); } export function trimToStartSentence(input) { @@ -1275,6 +1277,9 @@ export async function waitUntilCondition(condition, timeout = 1000, interval = 1 * uuidv4(); // '3e2fd9e1-0a7a-4f6d-9aaf-8a7a4babe7eb' */ export function uuidv4() { + if ('randomUUID' in crypto) { + return crypto.randomUUID(); + } return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8); diff --git a/public/style.css b/public/style.css index a74dbcbac..c24fd4288 100644 --- a/public/style.css +++ b/public/style.css @@ -77,6 +77,7 @@ --fontScale: 1; --mainFontSize: calc(var(--fontScale) * 15px); --mainFontFamily: "Noto Sans", "Noto Color Emoji", sans-serif; + --monoFontFamily: 'Noto Sans Mono', 'Courier New', Consolas, monospace; /* base variable for blur strength slider calculations */ --blurStrength: 10; @@ -444,7 +445,7 @@ small { } code { - font-family: Consolas, monospace; + font-family: var(--monoFontFamily); white-space: pre-wrap; /* word-wrap: break-word; */ border: 1px solid var(--SmartThemeBorderColor); @@ -1248,6 +1249,7 @@ select { left: var(--leftOffset); right: var(--rightOffset); z-index: 10000; + pointer-events: none; &.isFloating { @@ -1656,7 +1658,7 @@ body[data-stscript-style] .hljs.language-stscript { text-align: center; /* opacity: 0.6; */ white-space: nowrap; - font-family: monospace; + font-family: var(--monoFontFamily); line-height: calc(1.2em / 0.8); /* &:before { content: "["; } &:after { content: "]"; } */ @@ -1824,7 +1826,7 @@ body[data-stscript-style] .hljs.language-stscript { } >.alias { - font-family: monospace; + font-family: var(--monoFontFamily); &+.alias:before { content: ', '; @@ -1844,7 +1846,7 @@ body[data-stscript-style] .hljs.language-stscript { gap: 0.5em; >.name { - font-family: monospace; + font-family: var(--monoFontFamily); white-space: nowrap; /* color: var(--ac-color-text); */ } @@ -1853,7 +1855,7 @@ body[data-stscript-style] .hljs.language-stscript { display: flex; >.arguments { - font-family: monospace; + font-family: var(--monoFontFamily); .argument { white-space: nowrap; @@ -1943,7 +1945,7 @@ body[data-stscript-style] .hljs.language-stscript { } >.returns { - font-family: monospace; + font-family: var(--monoFontFamily); color: var(--ac-color-text); &:before { diff --git a/public/webfonts/NotoSansMono/noto-sans-mono-v30-100.woff2 b/public/webfonts/NotoSansMono/noto-sans-mono-v30-100.woff2 new file mode 100644 index 000000000..0e23ef8cd Binary files /dev/null and b/public/webfonts/NotoSansMono/noto-sans-mono-v30-100.woff2 differ diff --git a/public/webfonts/NotoSansMono/noto-sans-mono-v30-200.woff2 b/public/webfonts/NotoSansMono/noto-sans-mono-v30-200.woff2 new file mode 100644 index 000000000..6b9396d6f Binary files /dev/null and b/public/webfonts/NotoSansMono/noto-sans-mono-v30-200.woff2 differ diff --git a/public/webfonts/NotoSansMono/noto-sans-mono-v30-300.woff2 b/public/webfonts/NotoSansMono/noto-sans-mono-v30-300.woff2 new file mode 100644 index 000000000..e271fd128 Binary files /dev/null and b/public/webfonts/NotoSansMono/noto-sans-mono-v30-300.woff2 differ diff --git a/public/webfonts/NotoSansMono/noto-sans-mono-v30-500.woff2 b/public/webfonts/NotoSansMono/noto-sans-mono-v30-500.woff2 new file mode 100644 index 000000000..ba6135d55 Binary files /dev/null and b/public/webfonts/NotoSansMono/noto-sans-mono-v30-500.woff2 differ diff --git a/public/webfonts/NotoSansMono/noto-sans-mono-v30-600.woff2 b/public/webfonts/NotoSansMono/noto-sans-mono-v30-600.woff2 new file mode 100644 index 000000000..c8ca40080 Binary files /dev/null and b/public/webfonts/NotoSansMono/noto-sans-mono-v30-600.woff2 differ diff --git a/public/webfonts/NotoSansMono/noto-sans-mono-v30-700.woff2 b/public/webfonts/NotoSansMono/noto-sans-mono-v30-700.woff2 new file mode 100644 index 000000000..2b85d6aa9 Binary files /dev/null and b/public/webfonts/NotoSansMono/noto-sans-mono-v30-700.woff2 differ diff --git a/public/webfonts/NotoSansMono/noto-sans-mono-v30-800.woff2 b/public/webfonts/NotoSansMono/noto-sans-mono-v30-800.woff2 new file mode 100644 index 000000000..e3cf0bbab Binary files /dev/null and b/public/webfonts/NotoSansMono/noto-sans-mono-v30-800.woff2 differ diff --git a/public/webfonts/NotoSansMono/noto-sans-mono-v30-900.woff2 b/public/webfonts/NotoSansMono/noto-sans-mono-v30-900.woff2 new file mode 100644 index 000000000..962d8cd9a Binary files /dev/null and b/public/webfonts/NotoSansMono/noto-sans-mono-v30-900.woff2 differ diff --git a/public/webfonts/NotoSansMono/noto-sans-mono-v30-regular.woff2 b/public/webfonts/NotoSansMono/noto-sans-mono-v30-regular.woff2 new file mode 100644 index 000000000..e854afa92 Binary files /dev/null and b/public/webfonts/NotoSansMono/noto-sans-mono-v30-regular.woff2 differ diff --git a/public/webfonts/NotoSansMono/stylesheet.css b/public/webfonts/NotoSansMono/stylesheet.css new file mode 100644 index 000000000..80fdde3b5 --- /dev/null +++ b/public/webfonts/NotoSansMono/stylesheet.css @@ -0,0 +1,89 @@ +/* noto-sans-mono-100 - cyrillic_cyrillic-ext_greek_greek-ext_latin_latin-ext_vietnamese */ +@font-face { + font-display: swap; + font-family: 'Noto Sans Mono'; + font-style: normal; + font-weight: 100; + src: url('noto-sans-mono-v30-100.woff2') format('woff2'); + /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + +/* noto-sans-mono-200 - cyrillic_cyrillic-ext_greek_greek-ext_latin_latin-ext_vietnamese */ +@font-face { + font-display: swap; + font-family: 'Noto Sans Mono'; + font-style: normal; + font-weight: 200; + src: url('noto-sans-mono-v30-200.woff2') format('woff2'); + /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + +/* noto-sans-mono-300 - cyrillic_cyrillic-ext_greek_greek-ext_latin_latin-ext_vietnamese */ +@font-face { + font-display: swap; + font-family: 'Noto Sans Mono'; + font-style: normal; + font-weight: 300; + src: url('noto-sans-mono-v30-300.woff2') format('woff2'); + /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + +/* noto-sans-mono-regular - cyrillic_cyrillic-ext_greek_greek-ext_latin_latin-ext_vietnamese */ +@font-face { + font-display: swap; + font-family: 'Noto Sans Mono'; + font-style: normal; + font-weight: 400; + src: url('noto-sans-mono-v30-regular.woff2') format('woff2'); + /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + +/* noto-sans-mono-500 - cyrillic_cyrillic-ext_greek_greek-ext_latin_latin-ext_vietnamese */ +@font-face { + font-display: swap; + font-family: 'Noto Sans Mono'; + font-style: normal; + font-weight: 500; + src: url('noto-sans-mono-v30-500.woff2') format('woff2'); + /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + +/* noto-sans-mono-600 - cyrillic_cyrillic-ext_greek_greek-ext_latin_latin-ext_vietnamese */ +@font-face { + font-display: swap; + font-family: 'Noto Sans Mono'; + font-style: normal; + font-weight: 600; + src: url('noto-sans-mono-v30-600.woff2') format('woff2'); + /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + +/* noto-sans-mono-700 - cyrillic_cyrillic-ext_greek_greek-ext_latin_latin-ext_vietnamese */ +@font-face { + font-display: swap; + font-family: 'Noto Sans Mono'; + font-style: normal; + font-weight: 700; + src: url('noto-sans-mono-v30-700.woff2') format('woff2'); + /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + +/* noto-sans-mono-800 - cyrillic_cyrillic-ext_greek_greek-ext_latin_latin-ext_vietnamese */ +@font-face { + font-display: swap; + font-family: 'Noto Sans Mono'; + font-style: normal; + font-weight: 800; + src: url('noto-sans-mono-v30-800.woff2') format('woff2'); + /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + +/* noto-sans-mono-900 - cyrillic_cyrillic-ext_greek_greek-ext_latin_latin-ext_vietnamese */ +@font-face { + font-display: swap; + font-family: 'Noto Sans Mono'; + font-style: normal; + font-weight: 900; + src: url('noto-sans-mono-v30-900.woff2') format('woff2'); + /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} diff --git a/src/endpoints/translate.js b/src/endpoints/translate.js index d1e8f98ae..9541fc331 100644 --- a/src/endpoints/translate.js +++ b/src/endpoints/translate.js @@ -2,7 +2,7 @@ const fetch = require('node-fetch').default; const https = require('https'); const express = require('express'); const { readSecret, SECRET_KEYS } = require('./secrets'); -const { getConfigValue } = require('../util'); +const { getConfigValue, uuidv4 } = require('../util'); const { jsonParser } = require('../express-common'); const DEEPLX_URL_DEFAULT = 'http://127.0.0.1:1188/translate'; @@ -102,6 +102,54 @@ router.post('/google', jsonParser, async (request, response) => { } }); +router.post('/yandex', jsonParser, async (request, response) => { + const chunks = request.body.chunks; + const lang = request.body.lang; + + if (!chunks || !lang) { + return response.sendStatus(400); + } + + // reconstruct original text to log + let inputText = ''; + + const params = new URLSearchParams(); + for (const chunk of chunks) { + params.append('text', chunk); + inputText += chunk; + } + params.append('lang', lang); + const ucid = uuidv4().replaceAll('-', ''); + + console.log('Input text: ' + inputText); + + try { + const result = await fetch(`https://translate.yandex.net/api/v1/tr.json/translate?ucid=${ucid}&srv=android&format=text`, { + method: 'POST', + body: params, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + timeout: 0, + }); + + if (!result.ok) { + const error = await result.text(); + console.log('Yandex error: ', result.statusText, error); + return response.sendStatus(500); + } + + const json = await result.json(); + const translated = json.text.join(); + console.log('Translated text: ' + translated); + + return response.send(translated); + } catch (error) { + console.log('Translation error: ' + error.message); + return response.sendStatus(500); + } +}); + router.post('/lingva', jsonParser, async (request, response) => { try { const baseUrl = readSecret(request.user.directories, SECRET_KEYS.LINGVA_URL); diff --git a/src/util.js b/src/util.js index 326ad6a36..06f64da83 100644 --- a/src/util.js +++ b/src/util.js @@ -293,7 +293,14 @@ const color = { white: (mess) => color.byNum(mess, 37), }; +/** + * Gets a random UUIDv4 string. + * @returns {string} A UUIDv4 string + */ function uuidv4() { + if ('randomUUID' in crypto) { + return crypto.randomUUID(); + } return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8);