Compare commits
173 Commits
ebbd9a816d
...
ebabe03cb1
Author | SHA1 | Date |
---|---|---|
Cohee | ebabe03cb1 | |
Cohee | 1b77f9eab5 | |
Cohee | 2c9b67065e | |
Cohee | f743dd0b97 | |
Cohee | bc24ff841e | |
Cohee | 64b0123acf | |
Cohee | 35d853b851 | |
Cohee | 1966baad84 | |
Cohee | a398566b33 | |
PasserDreamer | 9f2c473040 | |
PasserDreamer | a73db4984a | |
PasserDreamer | 3498eb92bb | |
PasserDreamer | 455db18d71 | |
PasserDreamer | 2aeffe4095 | |
PasserDreamer | c4fe9749d5 | |
PasserDreamer | a0512585b1 | |
Cohee | c52bdb9a4a | |
Cohee | bbd9c89357 | |
Cohee | fb2190ace1 | |
Cohee | deb09bf5bf | |
Cohee | d951beb626 | |
Cohee | 748dd5f2e6 | |
Cohee | 75de4c8fcb | |
Cohee | 432be09583 | |
Succubyss | c822b9e2da | |
Cohee | c661fea07d | |
Cohee | 782f85e05d | |
Bronya-Rand | 9475147435 | |
Cohee | 909ec4191d | |
Cohee | 59d00cca74 | |
Cohee | 71a3e2c91b | |
Cohee | 719202ba12 | |
Cohee | 8ae4332110 | |
Cohee | 964f53273c | |
Cohee | 9e10022014 | |
Cohee | 2eaabe13e3 | |
steve green | 3389b5dd16 | |
Cohee | 3832afaeba | |
Cohee | e026ddf6be | |
hexa4ce | 116fa673c6 | |
Cohee | 517da9f972 | |
Cohee | 74256dc411 | |
Vincent Castellano | 1b23a62c13 | |
Wolfsblvt | 97de520f9a | |
Wolfsblvt | a6333f3285 | |
Cohee | 012f70336f | |
Cohee | 7fbed26c26 | |
Wolfsblvt | a94af2678b | |
Cohee | eb57289b2a | |
Wolfsblvt | 068b542c50 | |
Cohee | 912fd36e29 | |
Cohee | b7a91770dc | |
Cohee | f0af503b4a | |
Cohee | 3d023a5cf6 | |
Cohee | 1e2d1aa118 | |
Cohee | ccfd3606dc | |
Cohee | fe95e09c8b | |
Cohee | bac90edfad | |
Wolfsblvt | b7043a428f | |
Cohee | 5b47b83fe2 | |
Cohee | 7289ed72f8 | |
Cohee | c4936ed535 | |
Cohee | 8a5f05fb74 | |
LenAnderson | 36f7bc4aae | |
Cohee | d5869e3f90 | |
Cohee | 3b83d081db | |
steve green | 6861135925 | |
LenAnderson | b6f47c9927 | |
steve02081504 | 796cc3c60c | |
steve green | f1a57d76a2 | |
LenAnderson | 87b61f7cff | |
Cohee | 1999f607d6 | |
Wolfsblvt | bb2f553c46 | |
Wolfsblvt | 6c2dc6756b | |
Wolfsblvt | f7c12264e8 | |
Succubyss | 9ef3dea884 | |
Cohee | cd90e252bf | |
Cohee | ac2475fb26 | |
Wolfsblvt | 8f1a959da1 | |
Cohee | 8c55e1b05b | |
Cohee | a0bbee8b79 | |
Cohee | caf85ad040 | |
Cohee | 3999bee482 | |
Cohee | f0016b5368 | |
Cohee | 1dec93de8a | |
Cohee | caf236d60a | |
Cohee | c8ed8e06f1 | |
Cohee | aa845b4727 | |
Cohee | 1ebe5547d4 | |
Cohee | e2e7d5870a | |
Cohee | ea45d372f3 | |
Cohee | 3113109f0a | |
Bronya-Rand | 6ec51ff086 | |
Azariel Del Carmen | 9eae4d9739 | |
Bronya-Rand | 84aa746241 | |
Wolfsblvt | 00ce078630 | |
Wolfsblvt | 5426431adf | |
Wolfsblvt | 726ec0fbfc | |
Wolfsblvt | 094fc1f24b | |
Cohee | 49cb8daf7d | |
Cohee | 16660e995e | |
deffcolony | 8469f43285 | |
Wolfsblvt | 6865f84eb1 | |
Wolfsblvt | 036603c9e9 | |
Cohee | ab0f57aba3 | |
Cohee | 13c755c197 | |
LenAnderson | 5250d1fcaf | |
Cohee | 492f857012 | |
Cohee | f1a0462ca3 | |
Cohee | 6254ac6fbf | |
Cohee | 5207b3a7f0 | |
Cohee | 9e968de4e4 | |
LenAnderson | e3edb96568 | |
LenAnderson | dded42374c | |
Cohee | c561fb4fab | |
based | 67610b9f7f | |
Cohee | 14aa70eea8 | |
Cohee | 28da838bd1 | |
Cohee | fd18e0cc78 | |
Cohee | 2a30a74886 | |
Cohee | 6130ebb6d9 | |
Cohee | 64500bfb37 | |
Cohee | cc077732c4 | |
Cohee | cd47f3b238 | |
Cohee | 297519c401 | |
LenAnderson | 09d410ec48 | |
steve green | 1369025092 | |
Cohee | 5147233391 | |
Cohee | 38585cb6af | |
Cohee | f53775d3f5 | |
Cohee | 14ba7fc646 | |
Cohee | 5b7bfbaa98 | |
Len | 1d75b98393 | |
Cohee | c7d75b7789 | |
Cohee | 4ccedb939c | |
Cohee | 4bb463dd56 | |
Cohee | 0ba600bb2b | |
Cohee | 9ed6ee2161 | |
Cohee | c4ade296ae | |
Cohee | 0ed81e3b1a | |
steve02081504 | d1933be86a | |
steve02081504 | 15ff8de45c | |
steve02081504 | 45a080016e | |
steve02081504 | 78cf6e9086 | |
Cohee | 7d65a6e264 | |
Cohee | 91945ec77e | |
Cohee | 7b472f13af | |
Cohee | a93777e3b7 | |
Cohee | 2f310c72fa | |
Cohee | 6a4ee68113 | |
Cohee | e73b5713fd | |
Hydroerotic | 1f81086a21 | |
Hydroerotic | 3e48f4b805 | |
Hydroerotic | 432be2ee57 | |
steve green | f421139402 | |
RossAscends | 081223cc8f | |
Cohee | 27ccc6b090 | |
kingbri | 62faddac8d | |
kingbri | 6804e4c679 | |
Cohee | 10ee002091 | |
Cohee | 1430eb26ea | |
Wolfsblvt | eeaa52bf5d | |
steve02081504 | 7a2f6fb63f | |
Wolfsblvt | bb3ac095c4 | |
Wolfsblvt | e18d554489 | |
Wolfsblvt | a2625ecec6 | |
steve green | f6343436b4 | |
Wolfsblvt | f4bb4fe51e | |
Wolfsblvt | cf77b9e7ee | |
Wolfsblvt | eb273a1873 | |
Wolfsblvt | fda0e886e4 | |
Wolfsblvt | 5a45e64999 | |
Wolfsblvt | 70a2f71e33 |
|
@ -47,3 +47,4 @@ access.log
|
|||
public/css/user.css
|
||||
/plugins/
|
||||
/data
|
||||
/default/scaffold
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 338 KiB |
Binary file not shown.
Before Width: | Height: | Size: 598 KiB |
|
@ -107,14 +107,6 @@
|
|||
"filename": "default_Seraphina.png",
|
||||
"type": "character"
|
||||
},
|
||||
{
|
||||
"filename": "default_CodingSensei.png",
|
||||
"type": "character"
|
||||
},
|
||||
{
|
||||
"filename": "default_FluxTheCat.png",
|
||||
"type": "character"
|
||||
},
|
||||
{
|
||||
"filename": "Seraphina",
|
||||
"type": "sprites"
|
||||
|
|
|
@ -231,6 +231,7 @@
|
|||
"api_url_scale": "",
|
||||
"show_external_models": false,
|
||||
"assistant_prefill": "",
|
||||
"assistant_impersonation": "",
|
||||
"human_sysprompt_message": "Let's get started. Please generate your response based on the information and instructions provided above.",
|
||||
"use_ai21_tokenizer": false,
|
||||
"use_google_tokenizer": false,
|
||||
|
|
|
@ -387,14 +387,8 @@
|
|||
}
|
||||
],
|
||||
"tag_map": {
|
||||
"default_FluxTheCat.png": [
|
||||
"1345561466591"
|
||||
],
|
||||
"default_Seraphina.png": [
|
||||
"1345561466591"
|
||||
],
|
||||
"default_CodingSensei.png": [
|
||||
"1345561466591"
|
||||
]
|
||||
},
|
||||
"nai_settings": {
|
||||
|
@ -630,6 +624,7 @@
|
|||
"show_external_models": false,
|
||||
"proxy_password": "",
|
||||
"assistant_prefill": "",
|
||||
"assistant_impersonation": "",
|
||||
"use_ai21_tokenizer": false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
# Content Scaffolding
|
||||
|
||||
Content files in this folder will be copied for all users (old and new) on the server startup.
|
||||
|
||||
1. You **must** create an `index.json` file in `/default/scaffold` for it to work. The syntax is the same as for default content.
|
||||
2. All file paths should be relative to `/default/scaffold`, the use of subdirectories is allowed.
|
||||
3. Scaffolded files are copied first, so they override any of the default files (presets/settings/etc.) that have the same file name.
|
||||
|
||||
## Example
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"filename": "themes/Midnight.json",
|
||||
"type": "theme"
|
||||
},
|
||||
{
|
||||
"filename": "backgrounds/city.png",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"filename": "characters/Charlie.png",
|
||||
"type": "character"
|
||||
}
|
||||
]
|
||||
```
|
|
@ -12,7 +12,6 @@
|
|||
"dependencies": {
|
||||
"@agnai/sentencepiece-js": "^1.1.1",
|
||||
"@agnai/web-tokenizers": "^0.1.3",
|
||||
"@dqbd/tiktoken": "^1.0.13",
|
||||
"@zeldafan0225/ai_horde": "^4.0.1",
|
||||
"archiver": "^7.0.1",
|
||||
"bing-translate-api": "^2.9.1",
|
||||
|
@ -46,6 +45,7 @@
|
|||
"sanitize-filename": "^1.6.3",
|
||||
"sillytavern-transformers": "^2.14.6",
|
||||
"simple-git": "^3.19.1",
|
||||
"tiktoken": "^1.0.15",
|
||||
"vectra": "^0.2.2",
|
||||
"wavefile": "^11.0.0",
|
||||
"write-file-atomic": "^5.0.1",
|
||||
|
@ -82,10 +82,6 @@
|
|||
"version": "0.1.3",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@dqbd/tiktoken": {
|
||||
"version": "1.0.13",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
"version": "4.4.0",
|
||||
"dev": true,
|
||||
|
@ -4403,6 +4399,11 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tiktoken": {
|
||||
"version": "1.0.15",
|
||||
"resolved": "https://registry.npmjs.org/tiktoken/-/tiktoken-1.0.15.tgz",
|
||||
"integrity": "sha512-sCsrq/vMWUSEW29CJLNmPvWxlVp7yh2tlkAjpJltIKqp5CKf98ZNpdeHRmAlPVFlGEbswDc6SmI8vz64W/qErw=="
|
||||
},
|
||||
"node_modules/timm": {
|
||||
"version": "1.7.1",
|
||||
"license": "MIT"
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
"dependencies": {
|
||||
"@agnai/sentencepiece-js": "^1.1.1",
|
||||
"@agnai/web-tokenizers": "^0.1.3",
|
||||
"@dqbd/tiktoken": "^1.0.13",
|
||||
"@zeldafan0225/ai_horde": "^4.0.1",
|
||||
"archiver": "^7.0.1",
|
||||
"bing-translate-api": "^2.9.1",
|
||||
|
@ -36,6 +35,7 @@
|
|||
"sanitize-filename": "^1.6.3",
|
||||
"sillytavern-transformers": "^2.14.6",
|
||||
"simple-git": "^3.19.1",
|
||||
"tiktoken": "^1.0.15",
|
||||
"vectra": "^0.2.2",
|
||||
"wavefile": "^11.0.0",
|
||||
"write-file-atomic": "^5.0.1",
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -171,3 +171,78 @@
|
|||
.select2-results__option.select2-results__message::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.select2-selection__choice__display {
|
||||
/* Fix weird alignment on the left side */
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
/* Styling for choice remove icon */
|
||||
span.select2.select2-container .select2-selection__choice__remove {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
color: var(--SmartThemeBodyColor);
|
||||
background-color: var(--black50a);
|
||||
}
|
||||
|
||||
span.select2.select2-container .select2-selection__choice__remove:hover {
|
||||
color: var(--SmartThemeBodyColor);
|
||||
background-color: var(--white30a);
|
||||
}
|
||||
|
||||
/* Custom class to support styling to show clickable choice options */
|
||||
.select2_choice_clickable+span.select2-container .select2-selection__choice__display {
|
||||
cursor: pointer;
|
||||
}
|
||||
.select2_choice_clickable_buttonstyle+span.select2-container .select2-selection__choice__display {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
color: var(--SmartThemeBodyColor);
|
||||
background-color: var(--black50a);
|
||||
}
|
||||
|
||||
.select2_choice_clickable_buttonstyle+span.select2-container .select2-selection__choice__display:hover {
|
||||
background-color: var(--white30a);
|
||||
}
|
||||
|
||||
/* Custom class to support same line multi inputs of select2 controls */
|
||||
.select2_multi_sameline+span.select2-container .select2-selection--multiple {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}.select2_multi_sameline+span.select2-container .select2-selection--multiple .select2-search--inline {
|
||||
/* Allow search placeholder to take up all space if needed */
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.select2_multi_sameline+span.select2-container .select2-selection--multiple .select2-selection__rendered {
|
||||
/* Fix weird styling choice or huge margin around selected options */
|
||||
margin-block-start: 2px;
|
||||
margin-block-end: 2px;
|
||||
}
|
||||
|
||||
.select2_multi_sameline+span.select2-container .select2-selection--multiple .select2-search__field {
|
||||
/* Min height to reserve spacing */
|
||||
min-height: calc(var(--mainFontSize) + 13px);
|
||||
/* Min width to be clickable */
|
||||
min-width: 4em;
|
||||
align-content: center;
|
||||
/* Fix search textarea alignment issue with UL elements */
|
||||
margin-top: 0px;
|
||||
height: unset;
|
||||
/* Prevent height from jumping around when input is focused */
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.select2_multi_sameline+span.select2-container .select2-selection--multiple .select2-selection__rendered {
|
||||
/* Min height to reserve spacing */
|
||||
min-height: calc(var(--mainFontSize) + 13px);
|
||||
}
|
||||
|
||||
/* Make search bar invisible unless the select2 is active, to save space */
|
||||
.select2_multi_sameline+span.select2-container .select2-selection--multiple .select2-search--inline {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.select2_multi_sameline+span.select2-container.select2-container--focus .select2-selection--multiple .select2-search--inline {
|
||||
height: unset;
|
||||
}
|
||||
|
|
|
@ -193,7 +193,8 @@
|
|||
filter: brightness(75%) saturate(0.6);
|
||||
}
|
||||
|
||||
.tag_as_folder:hover {
|
||||
.tag_as_folder:hover,
|
||||
.tag_as_folder.flash {
|
||||
filter: brightness(150%) saturate(0.6) !important;
|
||||
}
|
||||
|
||||
|
|
|
@ -76,6 +76,12 @@
|
|||
.world_entry_form_control {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.world_entry_form_control .keyprimarytextpole,
|
||||
.world_entry_form_control .keysecondarytextpole {
|
||||
padding-right: 25px;
|
||||
}
|
||||
|
||||
.world_entry_thin_controls {
|
||||
|
@ -101,7 +107,7 @@
|
|||
height: auto;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
min-height: calc(var(--mainFontSize) + 13px);
|
||||
min-height: calc(var(--mainFontSize) + 14px);
|
||||
}
|
||||
|
||||
.delete_entry_button {
|
||||
|
@ -197,20 +203,57 @@
|
|||
display: none;
|
||||
}
|
||||
|
||||
#world_info+span.select2-container .select2-selection__choice__remove,
|
||||
#world_info+span.select2-container .select2-selection__choice__display {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
span.select2-container .select2-selection__choice__display:has(> .regex_item),
|
||||
span.select2-container .select2-results__option:has(> .result_block .regex_item) {
|
||||
background-color: #D27D2D30;
|
||||
}
|
||||
|
||||
.regex_item .regex_icon {
|
||||
background-color: var(--black30a);
|
||||
color: var(--SmartThemeBodyColor);
|
||||
background-color: var(--black50a);
|
||||
border: 1px solid var(--SmartThemeBorderColor);
|
||||
border-radius: 7px;
|
||||
font-weight: bold;
|
||||
font-size: calc(var(--mainFontSize) * 0.75);
|
||||
padding: 0px 3px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
#world_info+span.select2-container .select2-selection__choice__display {
|
||||
/* Fix weird alignment on the left side */
|
||||
margin-left: 1px;
|
||||
.select2-results__option .regex_item .regex_icon {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
#world_info+span.select2-container .select2-selection__choice__remove:hover,
|
||||
#world_info+span.select2-container .select2-selection__choice__display:hover {
|
||||
background-color: var(--white30a);
|
||||
.select2-results__option .item_count {
|
||||
margin-left: 10px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
select.keyselect+span.select2-container .select2-selection--multiple {
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.switch_input_type_icon {
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
height: 20px;
|
||||
width: fit-content;
|
||||
margin-right: 5px;
|
||||
margin-top: calc(5px + var(--mainFontSize));
|
||||
position: absolute;
|
||||
right: 0;
|
||||
padding: 1px;
|
||||
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
font-size: 1em;
|
||||
|
||||
opacity: 0.5;
|
||||
color: var(--SmartThemeBodyColor);
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.switch_input_type_icon:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -116,7 +116,7 @@
|
|||
</h4>
|
||||
<div class="flex-container flexNoGap">
|
||||
<select id="settings_preset_novel" class="flex1 text_pole" data-preset-manager-for="novel">
|
||||
<option value="gui" data-i18n="default">Default</option>
|
||||
<option value="gui" data-i18n="Default">Default</option>
|
||||
</select>
|
||||
<div class="flex-container marginLeft5 ">
|
||||
<input type="file" hidden data-preset-manager-file="novel" accept=".json, .settings">
|
||||
|
@ -134,7 +134,7 @@
|
|||
<h4 class="margin0"><span data-i18n="openaipresets">Chat Completion Presets</span></h4>
|
||||
<div class="flex-container flexNoGap">
|
||||
<select id="settings_preset_openai" class="flex1 text_pole" data-preset-manager-for="openai">
|
||||
<option value="gui" data-i18n="default">Default</option>
|
||||
<option value="gui" data-i18n="Default">Default</option>
|
||||
</select>
|
||||
<div class="flex-container marginLeft5 ">
|
||||
<input id="openai_preset_import_file" type="file" accept=".json,.settings" hidden />
|
||||
|
@ -246,7 +246,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="range-block">
|
||||
<div class="range-block-title" data-i18n="temperature">
|
||||
<div class="range-block-title" data-i18n="Temperature">
|
||||
Temperature
|
||||
</div>
|
||||
<div class="range-block-range-and-counter">
|
||||
|
@ -1239,7 +1239,7 @@
|
|||
<input class="neo-range-slider" type="range" id="no_repeat_ngram_size_textgenerationwebui" name="volume" min="0" max="20" step="1">
|
||||
<input class="neo-range-input" type="number" min="0" max="20" step="1" data-for="no_repeat_ngram_size_textgenerationwebui" id="no_repeat_ngram_size_counter_textgenerationwebui">
|
||||
</div>
|
||||
<div data-newbie-hidden data-tg-type="mancer, ooba, dreamgen" class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
|
||||
<div data-newbie-hidden data-tg-type="mancer, ooba, tabby, dreamgen" class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
|
||||
<small data-i18n="Min Length">Min Length</small>
|
||||
<input class="neo-range-slider" type="range" id="min_length_textgenerationwebui" name="volume" min="0" max="2000" step="1" />
|
||||
<input class="neo-range-input" type="number" min="0" max="2000" step="1" data-for="min_length_textgenerationwebui" id="min_length_counter_textgenerationwebui">
|
||||
|
@ -1267,45 +1267,44 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-newbie-hidden data-tg-type="mancer, ooba, koboldcpp, aphrodite, tabby" name="dryBlock" class="wide100p">
|
||||
<div data-newbie-hidden data-tg-type="ooba, llamacpp" name="dryBlock" class="wide100p">
|
||||
<h4 class="wide100p textAlignCenter">
|
||||
<label data-i18n="DRY Sampling">DRY Sampling</label>
|
||||
<div class=" fa-solid fa-circle-info opacity50p " data-i18n="[title]DRY Sampling" title="Allows you to apply the DRY repetition penalty as outlined in https://github.com/oobabooga/text-generation-webui/pull/5677. Set multiplier to 0 to disable"></div>
|
||||
</h4>
|
||||
<div data-tg-type="mancer, ooba, koboldcpp, aphrodite, tabby" class="flex-container flexFlowRow gap10px flexShrink">
|
||||
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<div class="flex-container flexFlowRow gap10px flexShrink">
|
||||
<div class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<small data-i18n="Allowed Length">Allowed Length</small>
|
||||
<input class="neo-range-slider" type="range" id="dry_allowed_length_textgenerationwebui" name="volume" min="1" max="20" step="1" />
|
||||
<input class="neo-range-input" type="number" min="1" max="20" step="1" data-for="dry_allowed_length_textgenerationwebui" id="dry_allowed_length_counter_textgenerationwebui">
|
||||
</div>
|
||||
<div data-tg-type="mancer, ooba, koboldcpp, aphrodite" data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<div class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<small data-i18n="Multiplier">Multiplier</small>
|
||||
<input class="neo-range-slider" type="range" id="dry_multiplier_textgenerationwebui" name="volume" min="0" max="10" step="0.01" />
|
||||
<input class="neo-range-input" type="number" min="0" max="5" step="0.01" data-for="dry_multiplier_textgenerationwebui" id="dry_multiplier_counter_textgenerationwebui">
|
||||
</div>
|
||||
<div data-tg-type="mancer, ooba, koboldcpp, aphrodite" data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<div class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<small data-i18n="Base">Base</small>
|
||||
<input class="neo-range-slider" type="range" id="dry_base_textgenerationwebui" name="volume" min="1" max="4" step="0.01" />
|
||||
<input class="neo-range-input" type="number" min="1" max="4" step="0.01" data-for="dry_base_textgenerationwebui" id="dry_base_counter_textgenerationwebui">
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block marginTop5">
|
||||
<div class="range-block-title textAlignCenter">
|
||||
<small data-i18n="Sequence Breakers">Sequence Breakers</small>
|
||||
</div>
|
||||
<div class="wide100p">
|
||||
<textarea id="dry_sequence_breakers_textgenerationwebui" class="text_pole textarea_compact" name="sequence_breakers" rows="3" data-i18n="[placeholder]JSON-serialized array of sequence breakers for DRY sampling." placeholder="JSON-serialized array of sequence breakers for DRY sampling."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block">
|
||||
<div class="range-block-title justifyLeft">
|
||||
<span data-i18n="DRY Sequence Breakers">DRY Sequence Breakers</span>
|
||||
</div>
|
||||
<div class="wide100p">
|
||||
<textarea id="dry_sequence_breakers_textgenerationwebui" class="text_pole textarea_compact" name="sequence_breakers" rows="3" data-i18n="[placeholder]Sequence breakers for DRY sampling can be added here as a comma separated list enclosed in brackets, where each element is enclosed in double quotation marks" placeholder="Sequence breakers for DRY sampling can be added here as a comma separated list enclosed in brackets, where each element is enclosed in double quotation marks."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div data-newbie-hidden data-tg-type="ooba, mancer, koboldcpp, tabby, llamacpp, aphrodite" name="dynaTempBlock" class="wide100p">
|
||||
<h4 class="wide100p textAlignCenter" data-i18n="DynaTemp">
|
||||
<div class="flex-container alignitemscenter" style="justify-content: center;">
|
||||
<h4 class="wide100p textAlignCenter">
|
||||
<div class="flex-container alignitemscenter justifyCenter">
|
||||
<div class="checkbox_label" for="dynatemp_textgenerationwebui">
|
||||
<input type="checkbox" id="dynatemp_textgenerationwebui" />
|
||||
<small data-i18n="dynatemp"></small>
|
||||
</div>
|
||||
<span style="text-align: center;" data-i18n="Dynamic Temperature">Dynamic Temperature</span>
|
||||
<span class="textAlignCenter" data-i18n="Dynamic Temperature">Dynamic Temperature</span>
|
||||
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Scale Temperature dynamically per token, based on the variation of probabilities" title="Scale Temperature dynamically per token, based on the variation of probabilities."></div>
|
||||
</div>
|
||||
</h4>
|
||||
|
@ -1711,7 +1710,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">
|
||||
|
@ -1720,6 +1719,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">
|
||||
|
@ -1740,8 +1749,9 @@
|
|||
<div class="range-block" data-source="makersuite">
|
||||
<label for="use_makersuite_sysprompt" class="checkbox_label widthFreeExpand">
|
||||
<input id="use_makersuite_sysprompt" type="checkbox" />
|
||||
<span data-i18n="Use system prompt (Gemini 1.5 pro+ only)">
|
||||
Use system prompt (Gemini 1.5 pro+ only)
|
||||
<span>
|
||||
<span data-i18n="Use system prompt">Use system prompt</span><br>
|
||||
<small data-i18n="(Gemini 1.5 Pro/Flash only)">(Gemini 1.5 Pro/Flash only)</small>
|
||||
</span>
|
||||
</label>
|
||||
<div class="toggle-description justifyLeft marginBot5">
|
||||
|
@ -1754,6 +1764,8 @@
|
|||
<div class="wide100p">
|
||||
<span id="claude_assistant_prefill_text" data-i18n="Assistant Prefill">Assistant Prefill</span>
|
||||
<textarea id="claude_assistant_prefill" class="text_pole textarea_compact" name="assistant_prefill autoSetHeight" rows="3" maxlength="10000" data-i18n="[placeholder]Start Claude's answer with..." placeholder="Start Claude's answer with..."></textarea>
|
||||
<span id="claude_assistant_impersonation_text" data-i18n="Assistant Impersonation Prefill">Assistant Impersonation Prefill</span>
|
||||
<textarea id="claude_assistant_impersonation" class="text_pole textarea_compact" name="assistant_impersonation autoSetHeight" rows="3" maxlength="10000" data-i18n="[placeholder]Start Claude's answer with..." placeholder="Start Claude's answer with..."></textarea>
|
||||
</div>
|
||||
<label for="claude_use_sysprompt" class="checkbox_label widthFreeExpand">
|
||||
<input id="claude_use_sysprompt" type="checkbox" />
|
||||
|
@ -2436,6 +2448,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>
|
||||
|
@ -2665,6 +2681,8 @@
|
|||
<h4 data-i18n="Google Model">Google Model</h4>
|
||||
<select id="model_google_select">
|
||||
<optgroup label="Latest">
|
||||
<!-- Doesn't work without "latest". Maybe my key is scuffed? -->
|
||||
<option value="gemini-1.5-flash-latest">Gemini 1.5 Flash</option>
|
||||
<!-- Points to 1.0, no default 1.5 endpoint -->
|
||||
<option value="gemini-pro">Gemini Pro</option>
|
||||
<option value="gemini-pro-vision">Gemini Pro Vision</option>
|
||||
|
@ -3341,7 +3359,7 @@
|
|||
<span data-i18n="Active World(s) for all chats"><small>Active World(s) for all chats</small></span>
|
||||
</div>
|
||||
<div class="range-block-range">
|
||||
<select id="world_info" multiple>
|
||||
<select id="world_info" class="select2_multi_sameline" multiple>
|
||||
<option value="" data-i18n="-- World Info not found --">-- World Info not found -- </option>
|
||||
</select>
|
||||
</div>
|
||||
|
@ -3457,8 +3475,8 @@
|
|||
</label>
|
||||
<label title="If the entry key consists of only one word, it would not be matched as part of other words" data-i18n="[title]If the entry key consists of only one word, it would not be matched as part of other words" class="checkbox_label flex1">
|
||||
<input id="world_info_match_whole_words" type="checkbox" />
|
||||
<small data-i18n="Match whole words" class="whitespacenowrap flex1">
|
||||
Match whole words
|
||||
<small data-i18n="Match Whole Words" class="whitespacenowrap flex1">
|
||||
Match Whole Words
|
||||
</small>
|
||||
</label>
|
||||
<label title="Only the entries with the most number of key matches will be selected for Inclusion Group filtering" data-i18n="[title]Only the entries with the most number of key matches will be selected for Inclusion Group filtering" class="checkbox_label flex1">
|
||||
|
@ -3611,7 +3629,7 @@
|
|||
<div class="flex-container">
|
||||
<span data-i18n="Chat Style:">Chat Style:</span><br>
|
||||
<select id="chat_display" class="widthNatural flex1 margin0">
|
||||
<option value="0" data-i18n="Default">Flat</span>
|
||||
<option value="0" data-i18n="Flat">Flat</span>
|
||||
<option value="1" data-i18n="Bubbles">Bubbles</option>
|
||||
<option value="2" data-i18n="Document">Document</option>
|
||||
</select>
|
||||
|
@ -3917,9 +3935,9 @@
|
|||
</label>
|
||||
|
||||
</div>
|
||||
<h4><span data-i18n="Miscellaneous">Miscellaneous</span></h4>
|
||||
|
||||
<div>
|
||||
<div name="MiscellaneousToggles">
|
||||
<h4><span data-i18n="Miscellaneous">Miscellaneous</span></h4>
|
||||
<div data-newbie-hidden class="flex-container">
|
||||
<div id="reload_chat" class="menu_button whitespacenowrap" data-i18n="[title]Reload and redraw the currently open chat" title="Reload and redraw the currently open chat.">
|
||||
<small data-i18n="Reload Chat">Reload Chat</small>
|
||||
|
@ -3994,8 +4012,8 @@
|
|||
<span data-i18n="Custom CSS">Custom CSS</span>
|
||||
<i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="customCSS" title="Expand the editor"></i>
|
||||
</h4>
|
||||
<div class="flex-container flexnowrap alignitemscenter">
|
||||
<textarea id="customCSS" class="text_pole margin0 margin-r5 textarea_compact monospace"></textarea>
|
||||
<div id="CustomCSS-textAreaBlock" class="flex-container flexnowrap alignitemscenter">
|
||||
<textarea id="customCSS" class="text_pole margin0 margin-r5 textarea_compact monospace" rows="8"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -4012,8 +4030,8 @@
|
|||
|
||||
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]The number of chat history messages to load before pagination." title="The number of chat history messages to load before pagination."></div>
|
||||
</small>
|
||||
<input class="neo-range-slider" type="range" id="chat_truncation" name="chat_truncation" min="0" max="1000" step="25">
|
||||
<input class="neo-range-input" type="number" min="0" max="1000" step="25" data-for="chat_truncation" id="chat_truncation_counter">
|
||||
<input class="neo-range-slider" type="range" id="chat_truncation" name="chat_truncation" min="0" max="1000" step="5">
|
||||
<input class="neo-range-input" type="number" min="0" max="1000" step="5" data-for="chat_truncation" id="chat_truncation_counter">
|
||||
<small data-i18n="(0 = All)">(0 = All)</small>
|
||||
</div>
|
||||
|
||||
|
@ -4096,7 +4114,7 @@
|
|||
<small class="fa-solid fa-circle-question note-link-small"></small>
|
||||
</a>
|
||||
</label>
|
||||
<label class="checkbox_label" for="forbid_external_media" title="Disalow embedded media from other domains in chat messages." data-i18n="[title]Disalow embedded media from other domains in chat messages">
|
||||
<label class="checkbox_label" for="forbid_external_media" title="Disallow embedded media from other domains in chat messages." data-i18n="[title]Disallow embedded media from other domains in chat messages">
|
||||
<input id="forbid_external_media" type="checkbox" />
|
||||
<small data-i18n="Forbid External Media">Forbid External Media</small>
|
||||
</label>
|
||||
|
@ -4145,7 +4163,82 @@
|
|||
</div>
|
||||
</div>
|
||||
</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>
|
||||
<select id="stscript_matching">
|
||||
<option data-i18n="Starts with" value="strict">Starts with</option>
|
||||
<option data-i18n="Includes" value="includes">Includes</option>
|
||||
<option data-i18n="Fuzzy" value="fuzzy">Fuzzy</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex1" title="Sets the style of the autocomplete." data-i18n="[title]Sets the style of the autocomplete.">
|
||||
<label for="stscript_autocomplete_style" data-i18n="Autocomplete Style"><small>Style</small></label>
|
||||
<div class="flex-container flexFlowRow alignItemsBaseline">
|
||||
<select id="stscript_autocomplete_style">
|
||||
<option data-i18n="Follow Theme" value="theme">Follow Theme</option>
|
||||
<option data-i18n="Dark" value="dark">Dark</option>
|
||||
<option data-i18n="Light" value="light">Light</option>
|
||||
</select>
|
||||
<!-- <div class="menu_button fa-solid fa-pen-to-square" title="Customize colors"></div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-container flexFlowColumn gap0" title="Sets the font size of the autocomplete." data-i18n="[title]Sets the font size of the autocomplete.">
|
||||
<label for="stscript_autocomplete_font_scale"><small>Font Scale</small></label>
|
||||
<input class="neo-range-slider" type="range" id="stscript_autocomplete_font_scale" min="0.5" max="2" step="0.01">
|
||||
<input class="neo-range-input" type="number" min="0.5" max="2" step="0.01" data-for="stscript_autocomplete_font_scale" id="stscript_autocomplete_font_scale_counter">
|
||||
</div>
|
||||
<div title="Sets the width of the autocomplete." data-i18n="[title]Sets the width of the autocomplete.">
|
||||
<label for="stscript_autocomplete_width" data-i18n="Autocomplete Width"><small>Width</small></label>
|
||||
<div class="doubleRangeContainer">
|
||||
<div class="doubleRangeInputContainer">
|
||||
<input type="range" id="stscript_autocomplete_width_left" min="0" max="2" step="1">
|
||||
<datalist id="stscript_autocomplete_width_left_values">
|
||||
<option value="0" label="input" title="chat input box"></option>
|
||||
<option value="1" label="chat" title="entire chat width"></option>
|
||||
<option value="2" label="full" title="full window width"></option>
|
||||
</datalist>
|
||||
</div>
|
||||
<div class="doubleRangeInputContainer">
|
||||
<input type="range" id="stscript_autocomplete_width_right" min="0" max="2" step="1">
|
||||
<datalist id="stscript_autocomplete_width_right_values">
|
||||
<option value="0" label="input" title="chat input box"></option>
|
||||
<option value="1" label="chat" title="entire chat width"></option>
|
||||
<option value="2" label="full" title="full window width"></option>
|
||||
</datalist>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div name="STscriptToggles">
|
||||
<h4 data-i18n="STscript Settings">STscript Settings</h4>
|
||||
<div title="Sets default flags for the STscript parser." data-i18n="[title]Sets default flags for the STscript parser.">
|
||||
<label data-i18n="Parser Flags"><small>Parser Flags</small></label>
|
||||
<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/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/usage/st-script/#replace-variable-macros" target="_blank" class="notes-link">
|
||||
<span class="fa-solid fa-circle-question note-link-span"></span>
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -4435,6 +4528,9 @@
|
|||
<option id="replace_update" data-i18n="Replace / Update">
|
||||
Replace / Update
|
||||
</option>
|
||||
<option id="import_tags" data-i18n="Import Tags">
|
||||
Import Tags
|
||||
</option>
|
||||
<!--<option id="dupe_button">
|
||||
Duplicate
|
||||
</option>
|
||||
|
@ -5146,7 +5242,11 @@
|
|||
</span>
|
||||
</small>
|
||||
<small class="textAlignCenter" data-i18n="Primary Keywords">Primary Keywords</small>
|
||||
<textarea class="text_pole keyprimarytextpole" name="key" rows="1" data-i18n="[placeholder]Comma separated (required)" placeholder="Comma separated (required)" maxlength="2000"></textarea>
|
||||
<select class="keyprimaryselect keyselect select2_multi_sameline" name="key" data-i18n="[placeholder]Keywords or Regexes" placeholder="Keywords or Regexes" multiple="multiple"></select>
|
||||
<textarea class="text_pole keyprimarytextpole mobile" name="key" rows="1" data-i18n="[placeholder]Comma separated list" placeholder="Comma separated list" maxlength="2000" style="display: none;"></textarea>
|
||||
<button type="button" class="switch_input_type_icon" tabindex="-1" title="Switch to plaintext mode" data-icon-on="✨" data-icon-off="⌨️" data-tooltip-on="Switch to fancy mode" data-tooltip-off="Switch to plaintext mode">
|
||||
⌨️
|
||||
</button>
|
||||
</div>
|
||||
<div class="world_entry_form_control">
|
||||
<small class="textAlignCenter" data-i18n="Logic">Logic</small>
|
||||
|
@ -5164,9 +5264,11 @@
|
|||
</span>
|
||||
</small>
|
||||
<small class="textAlignCenter" data-i18n="Optional Filter">Optional Filter</small>
|
||||
<div class="flex-container flexFlowRow alignitemscenter">
|
||||
<textarea class="text_pole keysecondarytextpole" name="keysecondary" rows="1" data-i18n="[placeholder]Comma separated (ignored if empty)" placeholder="Comma separated list" maxlength="2000"></textarea>
|
||||
</div>
|
||||
<select class="keysecondaryselect keyselect select2_multi_sameline" name="keysecondary" data-i18n="[placeholder]Keywords or Regexes (ignored if empty)" placeholder="Keywords or Regexes (ignored if empty)" multiple="multiple"></select>
|
||||
<textarea class="text_pole keysecondarytextpole mobile" name="keysecondary" rows="1" data-i18n="[placeholder]Comma separated list (ignored if empty)" placeholder="Comma separated list (ignored if empty)" maxlength="2000" style="display: none;"></textarea>
|
||||
<button type="button" class="switch_input_type_icon" tabindex="-1" title="Switch to plaintext mode" data-icon-on="✨" data-icon-off="⌨️" data-tooltip-on="Switch to fancy mode" data-tooltip-off="Switch to plaintext mode">
|
||||
⌨️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div name="perEntryOverridesBlock" class="flex-container wide100p alignitemscenter">
|
||||
|
@ -5228,6 +5330,12 @@
|
|||
Prevent further recursion (this entry will not activate others)
|
||||
</span>
|
||||
</label>
|
||||
<label class="checkbox flex-container alignitemscenter flexNoGap">
|
||||
<input type="checkbox" name="delay_until_recursion" />
|
||||
<span data-i18n="Delay until recursion (this entry can only be activated on recursive checking)">
|
||||
Delay until recursion (this entry can only be activated on recursive checking)
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</span>
|
||||
</small>
|
||||
|
@ -5257,7 +5365,7 @@
|
|||
</label>
|
||||
</div>
|
||||
<div class="range-block-range">
|
||||
<select name="characterFilter" multiple>
|
||||
<select name="characterFilter" class="select2_multi_sameline" multiple>
|
||||
<option value="">
|
||||
<span data-i18n="-- Characters not found --">-- Characters not found --</span>
|
||||
</option>
|
||||
|
@ -5268,7 +5376,7 @@
|
|||
<div class="flex-container justifySpaceBetween">
|
||||
<small for="group" data-i18n="Inclusion Group">
|
||||
Inclusion Group
|
||||
<a href="https://docs.sillytavern.app/usage/core-concepts/worldinfo/#inclusion-group" class="notes-link" target="_blank" title="Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered. Documentation: World Info - Inclusion Group" data-i18n="[title]Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered. Documentation: World Info - Inclusion Group">
|
||||
<a href="https://docs.sillytavern.app/usage/core-concepts/worldinfo/#inclusion-group" class="notes-link" target="_blank" title="Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered. Supports multiple comma-separated groups. Documentation: World Info - Inclusion Group" data-i18n="[title]Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered. Documentation: World Info - Inclusion Group">
|
||||
<span class="fa-solid fa-circle-question note-link-span"></span>
|
||||
</a>
|
||||
</small>
|
||||
|
@ -5543,8 +5651,14 @@
|
|||
<b data-i18n="SillyTavern is aimed at advanced users.">
|
||||
SillyTavern is aimed at advanced users.
|
||||
</b>
|
||||
<div data-i18n="If you're new to this, enable the simplified UI mode below.">
|
||||
If you're new to this, enable the simplified UI mode below.
|
||||
<div>
|
||||
<span data-i18n="If you're new to this, enable the simplified UI mode below.">
|
||||
If you're new to this, enable the simplified UI mode below.
|
||||
</span>
|
||||
<br>
|
||||
<span data-i18n="Change it later in the 'User Settings' panel.">
|
||||
Change it later in the 'User Settings' panel.
|
||||
</span>
|
||||
</div>
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" name="enable_simple_mode" />
|
||||
|
@ -5552,6 +5666,24 @@
|
|||
Enable simple UI mode
|
||||
</span>
|
||||
</label>
|
||||
<div class="textAlignCenter">
|
||||
<h3 data-i18n="Looking for AI characters?">
|
||||
Looking for AI characters?
|
||||
</h3>
|
||||
<span>
|
||||
<span class="menu_button menu_button_icon external_import_button">
|
||||
<i class="fa-solid fa-cloud-arrow-down"></i>
|
||||
<span>Import</span>
|
||||
</span>
|
||||
<span data-i18n="from supported sources or view">
|
||||
from supported sources or view
|
||||
</span>
|
||||
<span class="open_characters_library menu_button menu_button_icon">
|
||||
<i class="fa-solid fa-image-portrait"></i>
|
||||
<span data-i18n="Sample characters">Sample characters</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<h3 data-i18n="Your Persona">
|
||||
Your Persona
|
||||
</h3>
|
||||
|
@ -5559,6 +5691,7 @@
|
|||
<span data-i18n="Before you get started, you must select a persona name.">
|
||||
Before you get started, you must select a persona name.
|
||||
</span>
|
||||
<br>
|
||||
<span data-i18n="welcome_message_part_8">This can be changed at any time via the</span> <code><i class="fa-solid fa-face-smile"></i></code> <span data-i18n="welcome_message_part_9">icon.</span>
|
||||
</div>
|
||||
<h4 data-i18n="Persona Name:">Persona Name:</h4>
|
||||
|
@ -6076,6 +6209,15 @@
|
|||
</div>
|
||||
<textarea id="send_textarea" name="text" data-i18n="[no_connection_text]Not connected to API!;[connected_text]Type a message, or /? for help" placeholder="Not connected to API!" no_connection_text="Not connected to API!" connected_text="Type a message, or /? for help"></textarea>
|
||||
<div id="rightSendForm" class="alignContentCenter">
|
||||
<div id="stscript_continue" title="Continue script execution" class="stscript_btn stscript_continue" data-i18n="[title]Continue script execution">
|
||||
<i class="fa-solid fa-play"></i>
|
||||
</div>
|
||||
<div id="stscript_pause" title="Pause script execution" class="stscript_btn stscript_pause" data-i18n="[title]Pause script execution">
|
||||
<i class="fa-solid fa-pause"></i>
|
||||
</div>
|
||||
<div id="stscript_stop" title="Abort script execution" class="stscript_btn stscript_stop" data-i18n="[title]Abort script execution">
|
||||
<i class="fa-solid fa-stop"></i>
|
||||
</div>
|
||||
<div id="mes_stop" title="Abort request" class="mes_stop" data-i18n="[title]Abort request">
|
||||
<i class="fa-solid fa-circle-stop"></i>
|
||||
</div>
|
||||
|
@ -6239,31 +6381,11 @@
|
|||
<script type="module" src="lib/structured-clone/monkey-patch.js"></script>
|
||||
<script type="module" src="lib/swiped-events.js"></script>
|
||||
<script type="module" src="lib/eventemitter.js"></script>
|
||||
<script type="module" src="scripts/power-user.js"></script>
|
||||
<script type="module" src="scripts/i18n.js"></script>
|
||||
<script type="module" src="script.js"></script>
|
||||
<script type="module" src="scripts/world-info.js"></script>
|
||||
<script type="module" src="scripts/group-chats.js"></script>
|
||||
<script type="module" src="scripts/kai-settings.js"></script>
|
||||
<script type="module" src="scripts/textgen-settings.js"></script>
|
||||
<script type="module" src="scripts/textgen-models.js"></script>
|
||||
<script type="module" src="scripts/bookmarks.js"></script>
|
||||
<script type="module" src="scripts/horde.js"></script>
|
||||
<script type="module" src="scripts/RossAscends-mods.js"></script>
|
||||
<script type="module" src="scripts/slash-commands.js"></script>
|
||||
<script type="module" src="scripts/tags.js"></script>
|
||||
<script type="module" src="scripts/secrets.js"></script>
|
||||
<script type="module" src="scripts/extensions.js"></script>
|
||||
<script type="module" src="scripts/authors-note.js"></script>
|
||||
<script type="module" src="scripts/preset-manager.js"></script>
|
||||
<script type="module" src="scripts/filters.js"></script>
|
||||
<script type="module" src="scripts/personas.js"></script>
|
||||
<script type="module" src="scripts/server-history.js"></script>
|
||||
<script type="module" src="scripts/setting-search.js"></script>
|
||||
<script type="module" src="scripts/bulk-edit.js"></script>
|
||||
<script type="module" src="scripts/cfg-scale.js"></script>
|
||||
<script type="module" src="scripts/chats.js"></script>
|
||||
<script type="module" src="scripts/user.js"></script>
|
||||
<script type="module" src="scripts/setting-search.js"></script>
|
||||
<script type="module" src="scripts/server-history.js"></script>
|
||||
<script type="module" src="script.js"></script>
|
||||
<script>
|
||||
// Configure toast library:
|
||||
toastr.options.escapeHtml = true; // Prevent raw HTML inserts
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
"kobldpresets": "الإعدادات المسبقة لـ Kobold",
|
||||
"guikoboldaisettings": "إعدادات واجهة KoboldAI",
|
||||
"novelaipreserts": "الإعدادات المسبقة لـ NovelAI",
|
||||
"default": "افتراضي",
|
||||
"openaipresets": "الإعدادات المسبقة لـ OpenAI",
|
||||
"text gen webio(ooba) presets": "الإعدادات المسبقة لـ WebUI(ooba)",
|
||||
"response legth(tokens)": "طول الاستجابة (بعدد الاحرف او الرموز)",
|
||||
|
@ -62,7 +61,7 @@
|
|||
"Temperature": "درجة الحرارة",
|
||||
"Frequency Penalty": "عقوبة التكرار",
|
||||
"Presence Penalty": "عقوبة الوجود",
|
||||
"Top-p": "أعلى p",
|
||||
"Top-p": "أعلى p",
|
||||
"Display bot response text chunks as they are generated": "عرض النصوص لجظة بلحظة",
|
||||
"Top A": "أعلى A",
|
||||
"Typical Sampling": "عينة نموذجية",
|
||||
|
@ -101,7 +100,7 @@
|
|||
"Inserts jailbreak as a last system message.": "يدرج كسر الحظر كرسالة نظام أخيرة.",
|
||||
"This tells the AI to ignore its usual content restrictions.": "هذا يخبر الذكاء الاصطناعي بتجاهل القيود المعتادة على المحتوى.",
|
||||
"NSFW Encouraged": "NSFW مشجع",
|
||||
"Tell the AI that NSFW is allowed.": "قل للذكاء الاصطناعي أنه يُسمح بـ NSFW",
|
||||
"Tell the AI that NSFW is allowed.": "قل للذكاء الاصطناعي أنه يُسمح بـ NSFW",
|
||||
"NSFW Prioritized": "الأولوية للمحتوى غير مناسب للعمل",
|
||||
"NSFW prompt text goes first in the prompt to emphasize its effect.": "النص الغير مناسب للعمل يأتي أولاً في التعليمات لتأكيد تأثيره.",
|
||||
"Streaming": "البث المباشر ل",
|
||||
|
@ -141,7 +140,7 @@
|
|||
"Influences bot behavior in its responses": "يؤثر على سلوك الروبوت في ردوده.",
|
||||
"Connect": "الاتصال",
|
||||
"Test Message": "رسالة اختبار",
|
||||
"API": "واجهة برمجة التطبيقات (API)",
|
||||
"API": "واجهة برمجة التطبيقات (API)",
|
||||
"KoboldAI": "KoboldAI",
|
||||
"Use Horde": "استخدام Horde",
|
||||
"API url": "رابط API",
|
||||
|
@ -206,7 +205,7 @@
|
|||
"Scale API Key": "مفتاح API لـ Scale",
|
||||
"Alt Method": "طريقة بديلة",
|
||||
"AI21 API Key": "مفتاح API لـ AI21",
|
||||
"AI21 Model": "نموذج AI21",
|
||||
"AI21 Model": "نموذج AI21",
|
||||
"View API Usage Metrics": "عرض مقاييس استخدام واجهة برمجة التطبيقات",
|
||||
"Show External models (provided by API)": "عرض النماذج الخارجية (المقدمة من قبل واجهة برمجة التطبيقات)",
|
||||
"Bot": "روبوت:",
|
||||
|
@ -495,7 +494,6 @@
|
|||
"Global Lore First": "سرد العالم أولاً",
|
||||
"Recursive Scan": "فحص متكرر",
|
||||
"Case Sensitive": "حساس لحالة الأحرف",
|
||||
"Match whole words": "تطابق الكلمات الكاملة",
|
||||
"Alert On Overflow": "تنبيه عند التجاوز",
|
||||
"World/Lore Editor": "محرر العالم/السرد",
|
||||
"--- None ---": "--- لا شيء ---",
|
||||
|
@ -915,7 +913,7 @@
|
|||
"Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "استخدم المحلل النحوي المناسب لنماذج Google عبر واجهة برمجة التطبيقات الخاصة بهم. معالجة الإشارات الأولية بطيئة، ولكنها تقدم عداد رمز دقيق جدًا.",
|
||||
"Load koboldcpp order": "تحميل أمر koboldcpp",
|
||||
"Use Google Tokenizer": "استخدم محلل النحوي من Google"
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
"kobldpresets": "Kobold-Einstellungen von vorher",
|
||||
"guikoboldaisettings": "KoboldAI-Einstellungen für das Menü",
|
||||
"novelaipreserts": "NovelAI-Einstellungen von früher",
|
||||
"default": "Normal",
|
||||
"openaipresets": "OpenAI-Einstellungen von vorher",
|
||||
"text gen webio(ooba) presets": "WebUI(ooba)-Einstellungen für Texterstellung",
|
||||
"response legth(tokens)": "Länge der Antwort (Tokens)",
|
||||
|
@ -494,7 +493,6 @@
|
|||
"Global Lore First": "Globale Lore zuerst",
|
||||
"Recursive Scan": "Rekursive Suche",
|
||||
"Case Sensitive": "Groß-/Kleinschreibung beachten",
|
||||
"Match whole words": "Ganze Wörter abgleichen",
|
||||
"Alert On Overflow": "Warnung bei Überlauf",
|
||||
"World/Lore Editor": "Welt-/Lore-Editor",
|
||||
"--- None ---": "--- Keine ---",
|
||||
|
@ -917,5 +915,5 @@
|
|||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
"kobldpresets": "Preajustes de Kobold",
|
||||
"guikoboldaisettings": "Ajustes de interfaz de KoboldAI",
|
||||
"novelaipreserts": "Preajustes de NovelAI",
|
||||
"default": "Predeterminado",
|
||||
"openaipresets": "Preajustes de OpenAI",
|
||||
"text gen webio(ooba) presets": "Preajustes de Text Gen WebUI(ooba)",
|
||||
"response legth(tokens)": "Longitud de respuesta (tokens)",
|
||||
|
@ -494,7 +493,6 @@
|
|||
"Global Lore First": "Historia Global Primero",
|
||||
"Recursive Scan": "Escaneo Recursiva",
|
||||
"Case Sensitive": "Sensible a mayúsculas y minúsculas",
|
||||
"Match whole words": "Coincidir palabras completas",
|
||||
"Alert On Overflow": "Alerta en Desbordamiento",
|
||||
"World/Lore Editor": "Editor de Mundo/Historia",
|
||||
"--- None ---": "--- Ninguno ---",
|
||||
|
@ -891,6 +889,7 @@
|
|||
"Chat API": " API de chat",
|
||||
"and pick a character": "y elige un personaje",
|
||||
"in the chat bar": "en la barra de chat",
|
||||
"You can browse a list of bundled characters in the Download Extensions & Assets menu within": "Puedes explorar una lista de personajes incluidos en el menú de Download Extensions & Assets dentro de ",
|
||||
"Confused or lost?": "¿Confundido o perdido?",
|
||||
"click these icons!": "¡Haz clic en estos iconos!",
|
||||
"SillyTavern Documentation Site": "Sitio de documentación de SillyTavern",
|
||||
|
@ -914,7 +913,4 @@
|
|||
"Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "Usa el tokenizador apropiado para los modelos de Google a través de su API. Procesamiento de indicaciones más lento, pero ofrece un recuento de tokens mucho más preciso.",
|
||||
"Load koboldcpp order": "Cargar orden de koboldcpp",
|
||||
"Use Google Tokenizer": "Usar Tokenizador de Google"
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
"kobldpresets": "Préréglages de Kobold",
|
||||
"guikoboldaisettings": "Paramètres de l'interface utilisateur de KoboldAI",
|
||||
"novelaipreserts": "Préréglages de NovelAI",
|
||||
"default": "Par défaut",
|
||||
"openaipresets": "Préréglages d'OpenAI",
|
||||
"text gen webio(ooba) presets": "Préréglages de WebUI(ooba)",
|
||||
"response legth(tokens)": "Longueur de la réponse (en tokens)",
|
||||
|
@ -205,7 +204,7 @@
|
|||
"Scale API Key": "Clé API Scale",
|
||||
"Alt Method": "Méthode alternative",
|
||||
"AI21 API Key": "Clé API AI21",
|
||||
"AI21 Model": "Modèle AI21",
|
||||
"AI21 Model": "Modèle AI21",
|
||||
"View API Usage Metrics": "Afficher les mesures d'utilisation de l'API",
|
||||
"Show External models (provided by API)": "Afficher les modèles externes (fournis par l'API)",
|
||||
"Bot": "Bot",
|
||||
|
@ -494,7 +493,6 @@
|
|||
"Global Lore First": "Lore global d'abord",
|
||||
"Recursive Scan": "Analyse récursive",
|
||||
"Case Sensitive": "Sensible à la casse",
|
||||
"Match whole words": "Correspondre aux mots entiers",
|
||||
"Alert On Overflow": "Alerte en cas de dépassement",
|
||||
"World/Lore Editor": "Éditeur de monde/lore",
|
||||
"--- None ---": "--- Aucun ---",
|
||||
|
@ -914,5 +912,5 @@
|
|||
"Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "Utilisez le tokenizer approprié pour les modèles Google via leur API. Traitement des invitations plus lent, mais offre un décompte de jetons beaucoup plus précis.",
|
||||
"Load koboldcpp order": "Charger l'ordre koboldcpp",
|
||||
"Use Google Tokenizer": "Utiliser le tokenizer Google"
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
"kobldpresets": "Fyrir stillingar Kobold",
|
||||
"guikoboldaisettings": "Stillingar fyrir KoboldAI viðmót",
|
||||
"novelaipreserts": "Fyrir stillingar NovelAI",
|
||||
"default": "Sjálfgefið",
|
||||
"openaipresets": "Fyrir stillingar OpenAI",
|
||||
"text gen webio(ooba) presets": "Fyrir stillingar WebUI(ooba) textagerðar",
|
||||
"response legth(tokens)": "Lengd svars (í táknum eða stöfum)",
|
||||
|
@ -62,7 +61,7 @@
|
|||
"Temperature": "Hitastig",
|
||||
"Frequency Penalty": "Tíðnarefning",
|
||||
"Presence Penalty": "Tilkoma refning",
|
||||
"Top-p": "Topp-p",
|
||||
"Top-p": "Topp-p",
|
||||
"Display bot response text chunks as they are generated": "Birta bætir svarborðstextabrot þegar þau eru búnar til",
|
||||
"Top A": "Topp A",
|
||||
"Typical Sampling": "Venjuleg úrtaka",
|
||||
|
@ -495,7 +494,6 @@
|
|||
"Global Lore First": "Fyrst heimsfræði",
|
||||
"Recursive Scan": "Endurkvæm skoðun",
|
||||
"Case Sensitive": "Skilgreiningarfræðilegt",
|
||||
"Match whole words": "Nákvæm samræmi",
|
||||
"Alert On Overflow": "Viðvörun um flæði",
|
||||
"World/Lore Editor": "Heims-/fræðiritari",
|
||||
"--- None ---": "--- Engin ---",
|
||||
|
@ -915,5 +913,5 @@
|
|||
"Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "Notaðu rétta tokenizer fyrir Google módel með þeirra API. Hægri umhvörf fyrir hvöttavinnslu, en býður upp á miklu nákvæmari talningu á táknunum.",
|
||||
"Load koboldcpp order": "Hlaðið inn færslu af koboldcpp",
|
||||
"Use Google Tokenizer": "Notaðu Google Tokenizer"
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
"kobldpresets": "Preimpostazioni Kobold",
|
||||
"guikoboldaisettings": "Impostazioni dell'interfaccia KoboldAI",
|
||||
"novelaipreserts": "Preimpostazioni NovelAI",
|
||||
"default": "Predefinito",
|
||||
"openaipresets": "Preimpostazioni OpenAI",
|
||||
"text gen webio(ooba) presets": "Preimpostazioni WebUI(ooba) per la generazione di testo",
|
||||
"response legth(tokens)": "Lunghezza della risposta (token)",
|
||||
|
@ -495,7 +494,6 @@
|
|||
"Global Lore First": "Lore Globale Prima",
|
||||
"Recursive Scan": "Scansione Ricorsiva",
|
||||
"Case Sensitive": "Sensibile alle Maiuscole",
|
||||
"Match whole words": "Corrispondi a parole intere",
|
||||
"Alert On Overflow": "Avviso Su Overflow",
|
||||
"World/Lore Editor": "Editor di Mondo/Lore",
|
||||
"--- None ---": "--- Nessuno ---",
|
||||
|
@ -917,5 +915,5 @@
|
|||
"Use Google Tokenizer": "Usa il Tokenizer di Google"
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
"kobldpresets": "Koboldのプリセット",
|
||||
"guikoboldaisettings": "KoboldAIのGUI設定",
|
||||
"novelaipreserts": "NovelAIのプリセット",
|
||||
"default": "デフォルト",
|
||||
"openaipresets": "OpenAIのプリセット",
|
||||
"text gen webio(ooba) presets": "WebUI(ooba)のプリセット",
|
||||
"response legth(tokens)": "応答の長さ(トークン数)",
|
||||
|
@ -140,7 +139,7 @@
|
|||
"Influences bot behavior in its responses": "返信でボットの動作に影響を与えます",
|
||||
"Connect": "接続",
|
||||
"Test Message": "テストメッセージ",
|
||||
"API": "API",
|
||||
"API": "API",
|
||||
"KoboldAI": "KoboldAI",
|
||||
"Use Horde": "ホードを使用",
|
||||
"API url": "API URL",
|
||||
|
@ -494,7 +493,6 @@
|
|||
"Global Lore First": "グローバルロアを最初に表示",
|
||||
"Recursive Scan": "再帰的スキャン",
|
||||
"Case Sensitive": "大文字と小文字を区別する",
|
||||
"Match whole words": "完全な単語の一致",
|
||||
"Alert On Overflow": "オーバーフロー時に警告",
|
||||
"World/Lore Editor": "ワールド/ロアの編集",
|
||||
"--- None ---": "--- なし ---",
|
||||
|
@ -914,5 +912,5 @@
|
|||
"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": "koboldcppオーダーを読み込む",
|
||||
"Use Google Tokenizer": "Googleトークナイザーを使用"
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
"kobldpresets": "코볼드 사전 설정",
|
||||
"guikoboldaisettings": "KoboldAI 인터페이스 설정",
|
||||
"novelaipreserts": "NovelAI 사전 설정",
|
||||
"default": "기본값",
|
||||
"openaipresets": "OpenAI 사전 설정",
|
||||
"text gen webio(ooba) presets": "텍스트 생성 WebUI(ooba) 사전 설정",
|
||||
"response legth(tokens)": "응답 길이 (토큰)",
|
||||
|
@ -425,7 +424,7 @@
|
|||
"Start new chat": "새로운 채팅 시작",
|
||||
"View past chats": "과거 채팅 보기",
|
||||
"Delete messages": "메시지 삭제",
|
||||
"Impersonate": "사칭",
|
||||
"Impersonate": "대신 말하기",
|
||||
"Regenerate": "재생성",
|
||||
"PNG": "PNG",
|
||||
"JSON": "JSON",
|
||||
|
@ -495,7 +494,6 @@
|
|||
"Global Lore First": "글로벌 로어 우선",
|
||||
"Recursive Scan": "재귀 스캔",
|
||||
"Case Sensitive": "대소문자 구분",
|
||||
"Match whole words": "전체 단어 일치",
|
||||
"Alert On Overflow": "오버플로우 알림",
|
||||
"World/Lore Editor": "월드/로어 편집기",
|
||||
"--- None ---": "--- 없음 ---",
|
||||
|
@ -914,7 +912,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": "로딩할 메시지 수"
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
[
|
||||
{ "lang": "ar-sa", "display": "عربي (Arabic)" },
|
||||
{ "lang": "zh-cn", "display": "简体中文 (Chinese) (Simplified)" },
|
||||
{ "lang": "zh-tw", "display": "繁體中文 (Chinese) (Taiwan)" },
|
||||
{ "lang": "nl-nl", "display": "Nederlands (Dutch)" },
|
||||
{ "lang": "de-de", "display": "Deutsch (German)" },
|
||||
{ "lang": "fr-fr", "display": "Français (French)" },
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
"kobldpresets": "Kobold voorinstellingen",
|
||||
"guikoboldaisettings": "KoboldAI-interface-instellingen",
|
||||
"novelaipreserts": "NovelAI-voorinstellingen",
|
||||
"default": "Standaard",
|
||||
"openaipresets": "OpenAI-voorinstellingen",
|
||||
"text gen webio(ooba) presets": "WebUI(ooba)-voorinstellingen voor tekstgeneratie",
|
||||
"response legth(tokens)": "Reactielengte (tokens)",
|
||||
|
@ -495,7 +494,6 @@
|
|||
"Global Lore First": "Globale Lore Eerst",
|
||||
"Recursive Scan": "Recursieve Scan",
|
||||
"Case Sensitive": "Hoofdlettergevoelig",
|
||||
"Match whole words": "Hele woorden matchen",
|
||||
"Alert On Overflow": "Waarschuwing bij overloop",
|
||||
"World/Lore Editor": "Wereld/Lore Editor",
|
||||
"--- None ---": "--- Geen ---",
|
||||
|
@ -917,5 +915,5 @@
|
|||
"Use Google Tokenizer": "Google Tokenizer gebruiken"
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
"kobldpresets": "Configurações predefinidas do Kobold",
|
||||
"guikoboldaisettings": "Configurações da interface do KoboldAI",
|
||||
"novelaipreserts": "Configurações predefinidas do NovelAI",
|
||||
"default": "Padrão",
|
||||
"openaipresets": "Configurações predefinidas do OpenAI",
|
||||
"text gen webio(ooba) presets": "Configurações predefinidas do WebUI(ooba) para geração de texto",
|
||||
"response legth(tokens)": "Comprimento da resposta (tokens)",
|
||||
|
@ -493,7 +492,6 @@
|
|||
"Global Lore First": "Lore Global Primeiro",
|
||||
"Recursive Scan": "Verificação Recursiva",
|
||||
"Case Sensitive": "Sensível a Maiúsculas",
|
||||
"Match whole words": "Corresponder palavras inteiras",
|
||||
"Alert On Overflow": "Alerta em Overflow",
|
||||
"World/Lore Editor": "Editor de Mundo/Lore",
|
||||
"--- None ---": "--- Nenhum ---",
|
||||
|
@ -915,5 +913,5 @@
|
|||
"Use Google Tokenizer": "Usar Tokenizer do Google"
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
"kobldpresets": "Пресеты для Kobold",
|
||||
"guikoboldaisettings": "Настройки из интерфейса KoboldAI",
|
||||
"novelaipreserts": "Пресеты для NovelAI",
|
||||
"default": "По умолчанию",
|
||||
"openaipresets": "Пресеты для OpenAI",
|
||||
"text gen webio(ooba) presets": "Пресеты для WebUI(ooba)",
|
||||
"response legth(tokens)": "Ответ (в токенах)",
|
||||
|
@ -276,7 +275,7 @@
|
|||
"World Info": "Информация о мире",
|
||||
"Scan Depth": "Глубина сканирования",
|
||||
"Case-Sensitive": "С учетом регистра",
|
||||
"Match Whole Words": "Только целые слова",
|
||||
"Match Whole Words": "Только полное совпадение",
|
||||
"Use global setting": "Использовать глобальную настройку",
|
||||
"Yes": "Да",
|
||||
"No": "Нет",
|
||||
|
@ -495,7 +494,6 @@
|
|||
"Global Lore First": "Сначала глобальный лор",
|
||||
"Recursive Scan": "Рекурсивное сканирование",
|
||||
"Case Sensitive": "Учитывать регистр",
|
||||
"Match whole words": "Только полное совпадение",
|
||||
"Alert On Overflow": "Оповещение о переполнении",
|
||||
"World/Lore Editor": "Редактировать мир или лор",
|
||||
"--- None ---": "--- Отсутствует ---",
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
"kobldpresets": "Налаштування Kobold",
|
||||
"guikoboldaisettings": "З інтерфейсу KoboldAI",
|
||||
"novelaipreserts": "Налаштування NovelAI",
|
||||
"default": "За замовчуванням",
|
||||
"openaipresets": "Налаштування OpenAI",
|
||||
"text gen webio(ooba) presets": "Налаштування Text Completion",
|
||||
"response legth(tokens)": "Відповідь (токени)",
|
||||
|
@ -495,7 +494,6 @@
|
|||
"Global Lore First": "Глобальна інформація першою",
|
||||
"Recursive Scan": "Рекурсивне сканування",
|
||||
"Case Sensitive": "Чутливість до регістру",
|
||||
"Match whole words": "Відповідність цілим словам",
|
||||
"Alert On Overflow": "Сповіщення при переповненні",
|
||||
"World/Lore Editor": "Редактор світу/книги",
|
||||
"--- None ---": "--- Нічого ---",
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
"kobldpresets": "Cài đặt trước Kobold",
|
||||
"guikoboldaisettings": "Cài đặt giao diện KoboldAI",
|
||||
"novelaipreserts": "Cài đặt trước NovelAI",
|
||||
"default": "Mặc định",
|
||||
"openaipresets": "Cài đặt trước OpenAI",
|
||||
"text gen webio(ooba) presets": "Cài đặt trước WebUI(ooba) của máy tạo văn bản",
|
||||
"response legth(tokens)": "Độ dài phản hồi (trong các token)",
|
||||
|
@ -62,7 +61,7 @@
|
|||
"Temperature": "Nhiệt độ",
|
||||
"Frequency Penalty": "Phạt Tần số",
|
||||
"Presence Penalty": "Phạt Sự hiện",
|
||||
"Top-p": "Top-p",
|
||||
"Top-p": "Top-p",
|
||||
"Display bot response text chunks as they are generated": "Hiển thị các phần văn bản phản hồi của bot khi chúng được tạo ra",
|
||||
"Top A": "Top A",
|
||||
"Typical Sampling": "Mẫu Đại diện",
|
||||
|
@ -141,7 +140,7 @@
|
|||
"Influences bot behavior in its responses": "Ảnh hưởng đến hành vi của bot trong các phản hồi của nó",
|
||||
"Connect": "Kết nối",
|
||||
"Test Message": "Tin nhắn kiểm tra",
|
||||
"API": "Giao diện lập trình ứng dụng (API)",
|
||||
"API": "Giao diện lập trình ứng dụng (API)",
|
||||
"KoboldAI": "KoboldAI",
|
||||
"Use Horde": "Sử dụng Horde",
|
||||
"API url": "URL API",
|
||||
|
@ -206,7 +205,7 @@
|
|||
"Scale API Key": "Khóa API của Scale",
|
||||
"Alt Method": "Phương pháp thay thế",
|
||||
"AI21 API Key": "Khóa API của AI21",
|
||||
"AI21 Model": "Mô hình AI21",
|
||||
"AI21 Model": "Mô hình AI21",
|
||||
"View API Usage Metrics": "Xem số liệu sử dụng API",
|
||||
"Show External models (provided by API)": "Hiển thị các mô hình bên ngoài (do API cung cấp)",
|
||||
"Bot": "Bot:",
|
||||
|
@ -495,7 +494,6 @@
|
|||
"Global Lore First": "Sử liệu toàn cầu đầu tiên",
|
||||
"Recursive Scan": "Quét đệ quy",
|
||||
"Case Sensitive": "Phân biệt chữ hoa chữ thường",
|
||||
"Match whole words": "Khớp toàn bộ từ",
|
||||
"Alert On Overflow": "Cảnh báo khi tràn",
|
||||
"World/Lore Editor": "Trình soạn thảo Thế giới/Sử liệu",
|
||||
"--- None ---": "--- Không ---",
|
||||
|
@ -915,5 +913,5 @@
|
|||
"Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "Sử dụng bộ mã hóa phù hợp cho các mô hình của Google thông qua API của họ. Xử lý lời mời chậm hơn, nhưng cung cấp đếm token chính xác hơn nhiều.",
|
||||
"Load koboldcpp order": "Tải đơn hàng koboldcpp",
|
||||
"Use Google Tokenizer": "Sử dụng bộ mã hóa của Google"
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
"kobldpresets": "Kobold 预设",
|
||||
"guikoboldaisettings": "KoboldAI 用户界面设置",
|
||||
"novelaipreserts": "NovelAI 预设",
|
||||
"default": "默认",
|
||||
"openaipresets": "对话补全预设",
|
||||
"text gen webio(ooba) presets": "WebUI(ooba) 预设",
|
||||
"response legth(tokens)": "响应长度(以词符数计)",
|
||||
|
@ -495,7 +494,6 @@
|
|||
"Global Lore First": "全局世界书优先",
|
||||
"Recursive Scan": "递归扫描",
|
||||
"Case Sensitive": "区分大小写",
|
||||
"Match whole words": "完整匹配单词",
|
||||
"Alert On Overflow": "溢出警报",
|
||||
"World/Lore Editor": "世界书编辑器",
|
||||
"--- None ---": "--- 无 ---",
|
||||
|
|
File diff suppressed because it is too large
Load Diff
488
public/script.js
488
public/script.js
|
@ -158,7 +158,7 @@ import {
|
|||
import { debounce_timeout } from './scripts/constants.js';
|
||||
|
||||
import { ModuleWorkerWrapper, doDailyExtensionUpdatesCheck, extension_settings, getContext, loadExtensionSettings, renderExtensionTemplate, renderExtensionTemplateAsync, runGenerationInterceptors, saveMetadataDebounced, writeExtensionField } from './scripts/extensions.js';
|
||||
import { COMMENT_NAME_DEFAULT, executeSlashCommands, getSlashCommandsHelp, processChatSlashCommands, registerSlashCommand } from './scripts/slash-commands.js';
|
||||
import { COMMENT_NAME_DEFAULT, executeSlashCommands, executeSlashCommandsOnChatInput, getSlashCommandsHelp, isExecutingCommandsFromChatInput, pauseScriptExecution, processChatSlashCommands, registerSlashCommand, stopScriptExecution } from './scripts/slash-commands.js';
|
||||
import {
|
||||
tag_map,
|
||||
tags,
|
||||
|
@ -191,7 +191,7 @@ import { NOTE_MODULE_NAME, initAuthorsNote, metadata_keys, setFloatingPrompt, sh
|
|||
import { registerPromptManagerMigration } from './scripts/PromptManager.js';
|
||||
import { getRegexedString, regex_placement } from './scripts/extensions/regex/engine.js';
|
||||
import { initLogprobs, saveLogprobsForActiveMessage } from './scripts/logprobs.js';
|
||||
import { FILTER_TYPES, FilterHelper } from './scripts/filters.js';
|
||||
import { FILTER_STATES, FILTER_TYPES, FilterHelper, isFilterState } from './scripts/filters.js';
|
||||
import { getCfgPrompt, getGuidanceScale, initCfg } from './scripts/cfg-scale.js';
|
||||
import {
|
||||
force_output_sequence,
|
||||
|
@ -224,9 +224,13 @@ import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, de
|
|||
import { initPresetManager } from './scripts/preset-manager.js';
|
||||
import { evaluateMacros } from './scripts/macros.js';
|
||||
import { currentUser, setUserControls } from './scripts/user.js';
|
||||
import { callGenericPopup } from './scripts/popup.js';
|
||||
import { POPUP_TYPE, callGenericPopup } from './scripts/popup.js';
|
||||
import { renderTemplate, renderTemplateAsync } from './scripts/templates.js';
|
||||
import { ScraperManager } from './scripts/scrapers.js';
|
||||
import { SlashCommandParser } from './scripts/slash-commands/SlashCommandParser.js';
|
||||
import { SlashCommand } from './scripts/slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument } from './scripts/slash-commands/SlashCommandArgument.js';
|
||||
import { SlashCommandBrowser } from './scripts/slash-commands/SlashCommandBrowser.js';
|
||||
|
||||
//exporting functions and vars for mods
|
||||
export {
|
||||
|
@ -380,6 +384,7 @@ export const event_types = {
|
|||
MESSAGE_RECEIVED: 'message_received',
|
||||
MESSAGE_EDITED: 'message_edited',
|
||||
MESSAGE_DELETED: 'message_deleted',
|
||||
MESSAGE_UPDATED: 'message_updated',
|
||||
IMPERSONATE_READY: 'impersonate_ready',
|
||||
CHAT_CHANGED: 'chat_id_changed',
|
||||
GENERATION_STARTED: 'generation_started',
|
||||
|
@ -411,6 +416,7 @@ export const event_types = {
|
|||
GROUP_MEMBER_DRAFTED: 'group_member_drafted',
|
||||
WORLD_INFO_ACTIVATED: 'world_info_activated',
|
||||
TEXT_COMPLETION_SETTINGS_READY: 'text_completion_settings_ready',
|
||||
CHAT_COMPLETION_SETTINGS_READY: 'chat_completion_settings_ready',
|
||||
CHARACTER_FIRST_MESSAGE_SELECTED: 'character_first_message_selected',
|
||||
// TODO: Naming convention is inconsistent with other events
|
||||
CHARACTER_DELETED: 'characterDeleted',
|
||||
|
@ -418,6 +424,7 @@ export const event_types = {
|
|||
SMOOTH_STREAM_TOKEN_RECEIVED: 'smooth_stream_token_received',
|
||||
FILE_ATTACHMENT_DELETED: 'file_attachment_deleted',
|
||||
WORLDINFO_FORCE_ACTIVATE: 'worldinfo_force_activate',
|
||||
OPEN_CHARACTER_LIBRARY: 'open_character_library',
|
||||
};
|
||||
|
||||
export const eventSource = new EventEmitter();
|
||||
|
@ -463,6 +470,7 @@ let currentVersion = '0.0.0';
|
|||
export const default_ch_mes = 'Hello';
|
||||
let generatedPromptCache = '';
|
||||
let generation_started = new Date();
|
||||
/** @type {import('scripts/char-data.js').v1CharData[]} */
|
||||
export let characters = [];
|
||||
export let this_chid;
|
||||
let saveCharactersPage = 0;
|
||||
|
@ -788,7 +796,6 @@ export let novelai_setting_names;
|
|||
let abortController;
|
||||
|
||||
//css
|
||||
var css_mes_bg = $('<div class="mes"></div>').css('background');
|
||||
var css_send_form_display = $('<div id=send_form></div>').css('display');
|
||||
const MAX_GENERATION_LOOPS = 5;
|
||||
|
||||
|
@ -816,6 +823,28 @@ $.ajaxPrefilter((options, originalOptions, xhr) => {
|
|||
xhr.setRequestHeader('X-CSRF-Token', token);
|
||||
});
|
||||
|
||||
/**
|
||||
* Pings the STserver to check if it is reachable.
|
||||
* @returns {Promise<boolean>} True if the server is reachable, false otherwise.
|
||||
*/
|
||||
export async function pingServer() {
|
||||
try {
|
||||
const result = await fetch('api/ping', {
|
||||
method: 'GET',
|
||||
headers: getRequestHeaders(),
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error pinging server', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function firstLoadInit() {
|
||||
try {
|
||||
const tokenResponse = await fetch('/csrf-token');
|
||||
|
@ -1359,7 +1388,7 @@ function verifyCharactersSearchSortRule() {
|
|||
* @typedef {object} Entity - Object representing a display entity
|
||||
* @property {Character|Group|import('./scripts/tags.js').Tag|*} item - The item
|
||||
* @property {string|number} id - The id
|
||||
* @property {string} type - The type of this entity (character, group, tag)
|
||||
* @property {'character'|'group'|'tag'} type - The type of this entity (character, group, tag)
|
||||
* @property {Entity[]} [entities] - An optional list of entities relevant for this item
|
||||
* @property {number} [hidden] - An optional number representing how many hidden entities this entity contains
|
||||
*/
|
||||
|
@ -1434,7 +1463,11 @@ export function getEntitiesList({ doFilter = false, doSort = true } = {}) {
|
|||
const subCount = subEntities.length;
|
||||
subEntities = filterByTagState(entities, { subForEntity: entity });
|
||||
if (doFilter) {
|
||||
subEntities = entitiesFilter.applyFilters(subEntities, { clearScoreCache: false });
|
||||
// sub entities filter "hacked" because folder filter should not be applied there, so even in "only folders" mode characters show up
|
||||
subEntities = entitiesFilter.applyFilters(subEntities, { clearScoreCache: false, tempOverrides: { [FILTER_TYPES.FOLDER]: FILTER_STATES.UNDEFINED } });
|
||||
}
|
||||
if (doSort) {
|
||||
sortEntitiesList(subEntities);
|
||||
}
|
||||
entity.entities = subEntities;
|
||||
entity.hidden = subCount - subEntities.length;
|
||||
|
@ -1443,8 +1476,13 @@ export function getEntitiesList({ doFilter = false, doSort = true } = {}) {
|
|||
|
||||
// Second run filters, hiding whatever should be filtered later
|
||||
if (doFilter) {
|
||||
entities = filterByTagState(entities, { globalDisplayFilters: true });
|
||||
entities = entitiesFilter.applyFilters(entities);
|
||||
const beforeFinalEntities = filterByTagState(entities, { globalDisplayFilters: true });
|
||||
entities = entitiesFilter.applyFilters(beforeFinalEntities);
|
||||
|
||||
// Magic for folder filter. If that one is enabled, and no folders are display anymore, we remove that filter to actually show the characters.
|
||||
if (isFilterState(entitiesFilter.getFilterData(FILTER_TYPES.FOLDER), FILTER_STATES.SELECTED) && entities.filter(x => x.type == 'tag').length == 0) {
|
||||
entities = entitiesFilter.applyFilters(beforeFinalEntities, { tempOverrides: { [FILTER_TYPES.FOLDER]: FILTER_STATES.UNDEFINED } });
|
||||
}
|
||||
}
|
||||
|
||||
if (doSort) {
|
||||
|
@ -1496,6 +1534,18 @@ function getCharacterSource(chId = this_chid) {
|
|||
return `https://pygmalion.chat/${pygmalionId}`;
|
||||
}
|
||||
|
||||
const githubRepo = characters[chId]?.data?.extensions?.github_repo;
|
||||
|
||||
if (githubRepo) {
|
||||
return `https://github.com/${githubRepo}`;
|
||||
}
|
||||
|
||||
const sourceUrl = characters[chId]?.data?.extensions?.source_url;
|
||||
|
||||
if (sourceUrl) {
|
||||
return sourceUrl;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
|
@ -1694,6 +1744,7 @@ export async function reloadCurrentChat() {
|
|||
*/
|
||||
export function sendTextareaMessage() {
|
||||
if (is_send_press) return;
|
||||
if (isExecutingCommandsFromChatInput) return;
|
||||
|
||||
let generateType;
|
||||
// "Continue on send" is activated when the user hits "send" (or presses enter) on an empty chat box, and the last
|
||||
|
@ -2282,6 +2333,8 @@ export function substituteParams(content, _name1, _name2, _original, _group, _re
|
|||
environment.scenario = fields.scenario || '';
|
||||
environment.persona = fields.persona || '';
|
||||
environment.mesExamples = fields.mesExamples || '';
|
||||
environment.charVersion = fields.version || '';
|
||||
environment.char_version = fields.version || '';
|
||||
}
|
||||
|
||||
// Must be substituted last so that they're replaced inside {{description}}
|
||||
|
@ -2373,30 +2426,14 @@ export async function generateQuietPrompt(quiet_prompt, quietToLoud, skipWIAN, q
|
|||
* @param {string} message Text to be sent
|
||||
* @returns {Promise<boolean>} Whether the message sending was interrupted
|
||||
*/
|
||||
async function processCommands(message) {
|
||||
export async function processCommands(message) {
|
||||
if (!message || !message.trim().startsWith('/')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const previousText = String($('#send_textarea').val());
|
||||
const result = await executeSlashCommands(message);
|
||||
|
||||
if (!result || typeof result !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentText = String($('#send_textarea').val());
|
||||
|
||||
if (previousText === currentText) {
|
||||
$('#send_textarea').val(result.newText).trigger('input');
|
||||
}
|
||||
|
||||
// interrupt generation if the input was nothing but a command
|
||||
if (message.length > 0 && result?.newText.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return result?.interrupt;
|
||||
await executeSlashCommandsOnChatInput(message, {
|
||||
clearChatInput: true,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
export function sendSystemMessage(type, text, extra = {}) {
|
||||
|
@ -2426,6 +2463,14 @@ export function sendSystemMessage(type, text, extra = {}) {
|
|||
chat.push(newMessage);
|
||||
addOneMessage(newMessage);
|
||||
is_send_press = false;
|
||||
if (type == system_message_types.SLASH_COMMANDS) {
|
||||
const browser = new SlashCommandBrowser();
|
||||
const spinner = document.querySelector('#chat .last_mes .custom-slashHelp');
|
||||
const parent = spinner.parentElement;
|
||||
spinner.remove();
|
||||
browser.renderInto(parent);
|
||||
browser.search.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2579,10 +2624,10 @@ export function baseChatReplace(value, name1, name2) {
|
|||
|
||||
/**
|
||||
* Returns the character card fields for the current character.
|
||||
* @returns {{system: string, mesExamples: string, description: string, personality: string, persona: string, scenario: string, jailbreak: string}}
|
||||
* @returns {{system: string, mesExamples: string, description: string, personality: string, persona: string, scenario: string, jailbreak: string, version: string}}
|
||||
*/
|
||||
export function getCharacterCardFields() {
|
||||
const result = { system: '', mesExamples: '', description: '', personality: '', persona: '', scenario: '', jailbreak: '' };
|
||||
const result = { system: '', mesExamples: '', description: '', personality: '', persona: '', scenario: '', jailbreak: '', version: '' };
|
||||
const character = characters[this_chid];
|
||||
|
||||
if (!character) {
|
||||
|
@ -2597,6 +2642,7 @@ export function getCharacterCardFields() {
|
|||
result.persona = baseChatReplace(power_user.persona_description?.trim(), name1, name2);
|
||||
result.system = power_user.prefer_character_prompt ? baseChatReplace(characters[this_chid].data?.system_prompt?.trim(), name1, name2) : '';
|
||||
result.jailbreak = power_user.prefer_character_jailbreak ? baseChatReplace(characters[this_chid].data?.post_history_instructions?.trim(), name1, name2) : '';
|
||||
result.version = characters[this_chid].data?.character_version ?? '';
|
||||
|
||||
if (selected_group) {
|
||||
const groupCards = getGroupCharacterCards(selected_group, Number(this_chid));
|
||||
|
@ -2672,7 +2718,7 @@ class StreamingProcessor {
|
|||
let messageId = -1;
|
||||
|
||||
if (this.type == 'impersonate') {
|
||||
$('#send_textarea').val('').trigger('input');
|
||||
$('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles:true }));
|
||||
}
|
||||
else {
|
||||
await saveReply(this.type, text, true);
|
||||
|
@ -2708,7 +2754,7 @@ class StreamingProcessor {
|
|||
}
|
||||
|
||||
if (isImpersonate) {
|
||||
$('#send_textarea').val(processedText).trigger('input');
|
||||
$('#send_textarea').val(processedText)[0].dispatchEvent(new Event('input', { bubbles:true }));
|
||||
}
|
||||
else {
|
||||
let currentTime = new Date();
|
||||
|
@ -3055,7 +3101,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
|
|||
const interruptedByCommand = await processCommands(String($('#send_textarea').val()));
|
||||
|
||||
if (interruptedByCommand) {
|
||||
//$("#send_textarea").val('').trigger('input');
|
||||
//$("#send_textarea").val('')[0].dispatchEvent(new Event('input', { bubbles:true }));
|
||||
unblockGeneration(type);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
@ -3082,6 +3128,15 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
|
|||
}
|
||||
|
||||
if (!dryRun) {
|
||||
// Ping server to make sure it is still alive
|
||||
const pingResult = await pingServer();
|
||||
|
||||
if (!pingResult) {
|
||||
unblockGeneration(type);
|
||||
toastr.error('Verify that the server is running and accessible.', 'ST Server cannot be reached' );
|
||||
throw new Error('Server unreachable');
|
||||
}
|
||||
|
||||
// Hide swipes if not in a dry run.
|
||||
hideSwipeButtons();
|
||||
// If generated any message, set the flag to indicate it can't be recreated again.
|
||||
|
@ -3141,7 +3196,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
|
|||
if (type !== 'regenerate' && type !== 'swipe' && type !== 'quiet' && !isImpersonate && !dryRun) {
|
||||
is_send_press = true;
|
||||
textareaText = String($('#send_textarea').val());
|
||||
$('#send_textarea').val('').trigger('input');
|
||||
$('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles:true }));
|
||||
} else {
|
||||
textareaText = '';
|
||||
if (chat.length && chat[chat.length - 1]['is_user']) {
|
||||
|
@ -4103,7 +4158,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
|
|||
|
||||
if (getMessage.length > 0) {
|
||||
if (isImpersonate) {
|
||||
$('#send_textarea').val(getMessage).trigger('input');
|
||||
$('#send_textarea').val(getMessage)[0].dispatchEvent(new Event('input', { bubbles:true }));
|
||||
generatedPromptCache = '';
|
||||
await eventSource.emit(event_types.IMPERSONATE_READY, getMessage);
|
||||
}
|
||||
|
@ -5901,7 +5956,7 @@ async function doOnboarding(avatarId) {
|
|||
template.find('input[name="enable_simple_mode"]').on('input', function () {
|
||||
simpleUiMode = $(this).is(':checked');
|
||||
});
|
||||
let userName = await callPopup(template, 'input', currentUser?.name || name1);
|
||||
let userName = await callGenericPopup(template, POPUP_TYPE.INPUT, currentUser?.name || name1, { rows: 2 });
|
||||
|
||||
if (userName) {
|
||||
userName = userName.replace('\n', ' ');
|
||||
|
@ -6079,15 +6134,6 @@ export async function getSettings() {
|
|||
|
||||
//Load User's Name and Avatar
|
||||
initUserAvatar(settings.user_avatar);
|
||||
|
||||
firstRun = !!settings.firstRun;
|
||||
|
||||
if (firstRun) {
|
||||
hideLoader();
|
||||
await doOnboarding(user_avatar);
|
||||
firstRun = false;
|
||||
}
|
||||
|
||||
setPersonaDescription();
|
||||
|
||||
//Load the active character and group
|
||||
|
@ -6107,6 +6153,14 @@ export async function getSettings() {
|
|||
await loadExtensionSettings(settings, isVersionChanged);
|
||||
eventSource.emit(event_types.EXTENSION_SETTINGS_LOADED);
|
||||
}
|
||||
|
||||
firstRun = !!settings.firstRun;
|
||||
|
||||
if (firstRun) {
|
||||
hideLoader();
|
||||
await doOnboarding(user_avatar);
|
||||
firstRun = false;
|
||||
}
|
||||
}
|
||||
|
||||
settingsReady = true;
|
||||
|
@ -6172,20 +6226,14 @@ export async function saveSettings(type) {
|
|||
});
|
||||
}
|
||||
|
||||
export function setGenerationParamsFromPreset(preset, isMancerChange = null) {
|
||||
export function setGenerationParamsFromPreset(preset) {
|
||||
const needsUnlock = (preset.max_length ?? max_context) > MAX_CONTEXT_DEFAULT || (preset.genamt ?? amount_gen) > MAX_RESPONSE_DEFAULT;
|
||||
$('#max_context_unlocked').prop('checked', needsUnlock).trigger('change');
|
||||
|
||||
if (preset.genamt !== undefined) {
|
||||
amount_gen = preset.genamt;
|
||||
if (isMancerChange) {
|
||||
$('#amount_gen').attr('max', amount_gen);
|
||||
$('#amount_gen_counter').val($('#amount_gen').val());
|
||||
}
|
||||
else {
|
||||
$('#amount_gen').val(amount_gen);
|
||||
$('#amount_gen_counter').val(amount_gen);
|
||||
}
|
||||
$('#amount_gen').val(amount_gen);
|
||||
$('#amount_gen_counter').val(amount_gen);
|
||||
}
|
||||
|
||||
if (preset.max_length !== undefined) {
|
||||
|
@ -6243,6 +6291,8 @@ function updateMessage(div) {
|
|||
mes.extra.bias = null;
|
||||
}
|
||||
|
||||
chat_metadata['tainted'] = true;
|
||||
|
||||
return { mesBlock, text, mes, bias };
|
||||
}
|
||||
|
||||
|
@ -6311,6 +6361,7 @@ async function messageEditDone(div) {
|
|||
|
||||
this_edit_mes_id = undefined;
|
||||
await saveChatConditional();
|
||||
await eventSource.emit(event_types.MESSAGE_UPDATED, this_edit_mes_id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -6689,7 +6740,7 @@ export function select_selected_character(chid) {
|
|||
$('#description_textarea').val(characters[chid].description);
|
||||
$('#character_world').val(characters[chid].data?.extensions?.world || '');
|
||||
$('#creator_notes_textarea').val(characters[chid].data?.creator_notes || characters[chid].creatorcomment);
|
||||
$('#creator_notes_spoiler').html(DOMPurify.sanitize(converter.makeHtml(characters[chid].data?.creator_notes || characters[chid].creatorcomment), { MESSAGE_SANITIZE: true }));
|
||||
$('#creator_notes_spoiler').html(DOMPurify.sanitize(converter.makeHtml(substituteParams(characters[chid].data?.creator_notes) || characters[chid].creatorcomment), { MESSAGE_SANITIZE: true }));
|
||||
$('#character_version_textarea').val(characters[chid].data?.character_version || '');
|
||||
$('#system_prompt_textarea').val(characters[chid].data?.system_prompt || '');
|
||||
$('#post_history_instructions_textarea').val(characters[chid].data?.post_history_instructions || '');
|
||||
|
@ -7535,6 +7586,7 @@ window['SillyTavern'].getContext = function () {
|
|||
getCurrentChatId: getCurrentChatId,
|
||||
getRequestHeaders: getRequestHeaders,
|
||||
reloadCurrentChat: reloadCurrentChat,
|
||||
renameChat: renameChat,
|
||||
saveSettingsDebounced: saveSettingsDebounced,
|
||||
onlineStatus: online_status,
|
||||
maxContext: Number(max_context),
|
||||
|
@ -8257,6 +8309,75 @@ async function doDeleteChat() {
|
|||
$('#dialogue_popup_ok').trigger('click', { fromSlashCommand: true });
|
||||
}
|
||||
|
||||
async function doRenameChat(_, chatName) {
|
||||
if (!chatName) {
|
||||
toastr.warning('Name must be provided as an argument to rename this chat.');
|
||||
return;
|
||||
}
|
||||
|
||||
const currentChatName = getCurrentChatId();
|
||||
if (!currentChatName) {
|
||||
toastr.warning('No chat selected that can be renamed.');
|
||||
return;
|
||||
}
|
||||
|
||||
await renameChat(currentChatName, chatName);
|
||||
|
||||
toastr.success(`Successfully renamed chat to: ${chatName}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames the currently selected chat.
|
||||
* @param {string} oldFileName Old name of the chat (no JSONL extension)
|
||||
* @param {string} newName New name for the chat (no JSONL extension)
|
||||
*/
|
||||
export async function renameChat(oldFileName, newName) {
|
||||
const body = {
|
||||
is_group: !!selected_group,
|
||||
avatar_url: characters[this_chid]?.avatar,
|
||||
original_file: `${oldFileName}.jsonl`,
|
||||
renamed_file: `${newName}.jsonl`,
|
||||
};
|
||||
|
||||
try {
|
||||
showLoader();
|
||||
const response = await fetch('/api/chats/rename', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
headers: getRequestHeaders(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Unsuccessful request.');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
throw new Error('Server returned an error.');
|
||||
}
|
||||
|
||||
if (selected_group) {
|
||||
await renameGroupChat(selected_group, oldFileName, newName);
|
||||
}
|
||||
else {
|
||||
if (characters[this_chid].chat == oldFileName) {
|
||||
characters[this_chid].chat = newName;
|
||||
$('#selected_chat_pole').val(characters[this_chid].chat);
|
||||
await createOrEditCharacter();
|
||||
}
|
||||
}
|
||||
|
||||
await reloadCurrentChat();
|
||||
} catch {
|
||||
hideLoader();
|
||||
await delay(500);
|
||||
await callPopup('An error has occurred. Chat was not renamed.', 'text');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* /getchatname` slash command
|
||||
*/
|
||||
|
@ -8443,19 +8564,133 @@ jQuery(async function () {
|
|||
toastr.success('Chat and settings saved.');
|
||||
}
|
||||
|
||||
registerSlashCommand('dupe', DupeChar, [], '– duplicates the currently selected character', true, true);
|
||||
registerSlashCommand('api', connectAPISlash, [], `<span class="monospace">(${Object.keys(CONNECT_API_MAP).join(', ')})</span> – connect to an API`, true, true);
|
||||
registerSlashCommand('impersonate', doImpersonate, ['imp'], '<span class="monospace">[prompt]</span> – calls an impersonation response, with an optional additional prompt', true, true);
|
||||
registerSlashCommand('delchat', doDeleteChat, [], '– deletes the current chat', true, true);
|
||||
registerSlashCommand('getchatname', doGetChatName, [], '– returns the name of the current chat file into the pipe', false, true);
|
||||
registerSlashCommand('closechat', doCloseChat, [], '– closes the current chat', true, true);
|
||||
registerSlashCommand('panels', doTogglePanels, ['togglepanels'], '– toggle UI panels on/off', true, true);
|
||||
registerSlashCommand('forcesave', doForceSave, [], '– forces a save of the current chat and settings', true, true);
|
||||
registerSlashCommand('instruct', selectInstructCallback, [], '<span class="monospace">(name)</span> – selects instruct mode preset by name. Gets the current instruct if no name is provided', true, true);
|
||||
registerSlashCommand('instruct-on', enableInstructCallback, [], '– enables instruct mode', true, true);
|
||||
registerSlashCommand('instruct-off', disableInstructCallback, [], '– disables instruct mode', true, true);
|
||||
registerSlashCommand('context', selectContextCallback, [], '<span class="monospace">(name)</span> – selects context template by name. Gets the current template if no name is provided', true, true);
|
||||
registerSlashCommand('chat-manager', () => $('#option_select_chat').trigger('click'), ['chat-history', 'manage-chats'], '– opens the chat manager for the current character/group', true, true);
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'dupe',
|
||||
callback: DupeChar,
|
||||
helpString: 'Duplicates the currently selected character.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'api',
|
||||
callback: connectAPISlash,
|
||||
namedArgumentList: [],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'API to connect to',
|
||||
[ARGUMENT_TYPE.STRING],
|
||||
true,
|
||||
false,
|
||||
null,
|
||||
Object.keys(CONNECT_API_MAP),
|
||||
),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Connect to an API.
|
||||
</div>
|
||||
<div>
|
||||
<strong>Available APIs:</strong>
|
||||
<pre><code>${Object.keys(CONNECT_API_MAP).join(', ')}</code></pre>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'impersonate',
|
||||
callback: doImpersonate,
|
||||
aliases: ['imp'],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'prompt', [ARGUMENT_TYPE.STRING], false,
|
||||
),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Calls an impersonation response, with an optional additional prompt.
|
||||
</div>
|
||||
<div>
|
||||
<strong>Example:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code class="language-stscript">/impersonate What is the meaning of life?</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'delchat',
|
||||
callback: doDeleteChat,
|
||||
helpString: 'Deletes the current chat.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'renamechat',
|
||||
callback: doRenameChat,
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'new chat name', [ARGUMENT_TYPE.STRING], true,
|
||||
),
|
||||
],
|
||||
helpString: 'Renames the current chat.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'getchatname',
|
||||
callback: doGetChatName,
|
||||
returns: 'chat file name',
|
||||
helpString: 'Returns the name of the current chat file into the pipe.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'closechat',
|
||||
callback: doCloseChat,
|
||||
helpString: 'Closes the current chat.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'panels',
|
||||
callback: doTogglePanels,
|
||||
aliases: ['togglepanels'],
|
||||
helpString: 'Toggle UI panels on/off',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'forcesave',
|
||||
callback: doForceSave,
|
||||
helpString: 'Forces a save of the current chat and settings',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'instruct',
|
||||
callback: selectInstructCallback,
|
||||
returns: 'current preset',
|
||||
namedArgumentList: [],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'name', [ARGUMENT_TYPE.STRING], false,
|
||||
),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Selects instruct mode preset by name. Gets the current instruct if no name is provided.
|
||||
</div>
|
||||
<div>
|
||||
<strong>Example:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code class="language-stscript">/instruct creative</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'instruct-on',
|
||||
callback: enableInstructCallback,
|
||||
helpString: 'Enables instruct mode.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'instruct-off',
|
||||
callback: disableInstructCallback,
|
||||
helpString: 'Disables instruct mode',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'context',
|
||||
callback: selectContextCallback,
|
||||
returns: 'template name',
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'name', [ARGUMENT_TYPE.STRING], false,
|
||||
),
|
||||
],
|
||||
helpString: 'Selects context template by name. Gets the current template if no name is provided',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'chat-manager',
|
||||
callback: () => $('#option_select_chat').trigger('click'),
|
||||
aliases: ['chat-history', 'manage-chats'],
|
||||
helpString: 'Opens the chat manager for the current character/group.',
|
||||
}));
|
||||
|
||||
setTimeout(function () {
|
||||
$('#groupControlsToggle').trigger('click');
|
||||
|
@ -8585,19 +8820,17 @@ jQuery(async function () {
|
|||
}
|
||||
$('.mes').children('.del_checkbox').each(function () {
|
||||
$(this).prop('checked', false);
|
||||
$(this).parent().css('background', css_mes_bg);
|
||||
$(this).parent().removeClass('selected');
|
||||
});
|
||||
$(this).css('background', '#600'); //sets the bg of the mes selected for deletion
|
||||
$(this).addClass('selected'); //sets the bg of the mes selected for deletion
|
||||
var i = Number($(this).attr('mesid')); //checks the message ID in the chat
|
||||
this_del_mes = i;
|
||||
//as long as the current message ID is less than the total chat length
|
||||
while (i < chat.length) {
|
||||
//as long as the current message ID is less than the total chat length
|
||||
$('.mes[mesid=\'' + i + '\']').css('background', '#600'); //sets the bg of the all msgs BELOW the selected .mes
|
||||
$('.mes[mesid=\'' + i + '\']')
|
||||
.children('.del_checkbox')
|
||||
.prop('checked', true);
|
||||
//sets the bg of the all msgs BELOW the selected .mes
|
||||
$(`.mes[mesid="${i}"]`).addClass('selected');
|
||||
$(`.mes[mesid="${i}"]`).children('.del_checkbox').prop('checked', true);
|
||||
i++;
|
||||
//console.log(i);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -8833,69 +9066,26 @@ jQuery(async function () {
|
|||
|
||||
$(document).on('click', '.renameChatButton', async function (e) {
|
||||
e.stopPropagation();
|
||||
const old_filenamefull = $(this).closest('.select_chat_block_wrapper').find('.select_chat_block_filename').text();
|
||||
const old_filename = old_filenamefull.replace('.jsonl', '');
|
||||
const oldFileNameFull = $(this).closest('.select_chat_block_wrapper').find('.select_chat_block_filename').text();
|
||||
const oldFileName = oldFileNameFull.replace('.jsonl', '');
|
||||
|
||||
const popupText = `<h3>Enter the new name for the chat:<h3>
|
||||
<small>!!Using an existing filename will produce an error!!<br>
|
||||
This will break the link between checkpoint chats.<br>
|
||||
No need to add '.jsonl' at the end.<br>
|
||||
</small>`;
|
||||
const newName = await callPopup(popupText, 'input', old_filename);
|
||||
const newName = await callPopup(popupText, 'input', oldFileName);
|
||||
|
||||
if (!newName || newName == old_filename) {
|
||||
if (!newName || newName == oldFileName) {
|
||||
console.log('no new name found, aborting');
|
||||
return;
|
||||
}
|
||||
|
||||
const body = {
|
||||
is_group: !!selected_group,
|
||||
avatar_url: characters[this_chid]?.avatar,
|
||||
original_file: `${old_filename}.jsonl`,
|
||||
renamed_file: `${newName}.jsonl`,
|
||||
};
|
||||
await renameChat(oldFileName, newName);
|
||||
|
||||
try {
|
||||
showLoader();
|
||||
const response = await fetch('/api/chats/rename', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
headers: getRequestHeaders(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Unsuccessful request.');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
throw new Error('Server returned an error.');
|
||||
}
|
||||
|
||||
if (selected_group) {
|
||||
await renameGroupChat(selected_group, old_filename, newName);
|
||||
}
|
||||
else {
|
||||
if (characters[this_chid].chat == old_filename) {
|
||||
characters[this_chid].chat = newName;
|
||||
$('#selected_chat_pole').val(characters[this_chid].chat);
|
||||
await createOrEditCharacter();
|
||||
}
|
||||
}
|
||||
|
||||
await reloadCurrentChat();
|
||||
|
||||
await delay(250);
|
||||
$('#option_select_chat').trigger('click');
|
||||
$('#options').hide();
|
||||
} catch {
|
||||
hideLoader();
|
||||
await delay(500);
|
||||
await callPopup('An error has occurred. Chat was not renamed.', 'text');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
await delay(250);
|
||||
$('#option_select_chat').trigger('click');
|
||||
$('#options').hide();
|
||||
});
|
||||
|
||||
$(document).on('click', '.exportChatButton, .exportRawChatButton', async function (e) {
|
||||
|
@ -9201,7 +9391,7 @@ jQuery(async function () {
|
|||
$('.del_checkbox').each(function () {
|
||||
$(this).css('display', 'none');
|
||||
$(this).parent().children('.for_checkbox').css('display', 'block');
|
||||
$(this).parent().css('background', css_mes_bg);
|
||||
$(this).parent().removeClass('selected');
|
||||
$(this).prop('checked', false);
|
||||
});
|
||||
showSwipeButtons();
|
||||
|
@ -9216,22 +9406,19 @@ jQuery(async function () {
|
|||
$('.del_checkbox').each(function () {
|
||||
$(this).css('display', 'none');
|
||||
$(this).parent().children('.for_checkbox').css('display', 'block');
|
||||
$(this).parent().css('background', css_mes_bg);
|
||||
$(this).parent().removeClass('selected');
|
||||
$(this).prop('checked', false);
|
||||
});
|
||||
|
||||
if (this_del_mes >= 0) {
|
||||
$('.mes[mesid=\'' + this_del_mes + '\']')
|
||||
.nextAll('div')
|
||||
.remove();
|
||||
$('.mes[mesid=\'' + this_del_mes + '\']').remove();
|
||||
$(`.mes[mesid="${this_del_mes}"]`).nextAll('div').remove();
|
||||
$(`.mes[mesid="${this_del_mes}"]`).remove();
|
||||
chat.length = this_del_mes;
|
||||
await saveChatConditional();
|
||||
var $textchat = $('#chat');
|
||||
$textchat.scrollTop($textchat[0].scrollHeight);
|
||||
chatElement.scrollTop(chatElement[0].scrollHeight);
|
||||
eventSource.emit(event_types.MESSAGE_DELETED, chat.length);
|
||||
$('#chat .mes').removeClass('last_mes');
|
||||
$('#chat .mes').last().addClass('last_mes');
|
||||
$('#chat .mes').eq(-2).removeClass('last_mes');
|
||||
} else {
|
||||
console.log('this_del_mes is not >= 0, not deleting');
|
||||
}
|
||||
|
@ -9838,11 +10025,22 @@ jQuery(async function () {
|
|||
streamingProcessor = null;
|
||||
}
|
||||
if (abortController) {
|
||||
abortController.abort();
|
||||
abortController.abort('Clicked stop button');
|
||||
hideStopButton();
|
||||
}
|
||||
eventSource.emit(event_types.GENERATION_STOPPED);
|
||||
activateSendButtons();
|
||||
});
|
||||
|
||||
$(document).on('click', '#form_sheld .stscript_continue', function () {
|
||||
pauseScriptExecution();
|
||||
});
|
||||
|
||||
$(document).on('click', '#form_sheld .stscript_pause', function () {
|
||||
pauseScriptExecution();
|
||||
});
|
||||
|
||||
$(document).on('click', '#form_sheld .stscript_stop', function () {
|
||||
stopScriptExecution();
|
||||
});
|
||||
|
||||
$('.drawer-toggle').on('click', function () {
|
||||
|
@ -10140,6 +10338,9 @@ jQuery(async function () {
|
|||
$('#character_replace_file').off('change').on('change', uploadReplacementCard).trigger('click');
|
||||
}
|
||||
} break;
|
||||
case 'import_tags':{
|
||||
await importTags(characters[this_chid]);
|
||||
} break;
|
||||
/*case 'delete_button':
|
||||
popup_type = "del_ch";
|
||||
callPopup(`
|
||||
|
@ -10228,7 +10429,7 @@ jQuery(async function () {
|
|||
userStatsHandler();
|
||||
});
|
||||
|
||||
$('#external_import_button').on('click', async () => {
|
||||
$(document).on('click', '.external_import_button, #external_import_button', async () => {
|
||||
const html = `<h3>Enter the URL of the content to import</h3>
|
||||
Supported sources:<br>
|
||||
<ul class="justifyLeft">
|
||||
|
@ -10239,7 +10440,7 @@ jQuery(async function () {
|
|||
<li>AICharacterCard.com Character (Direct Link or ID)<br>Example: <tt>AICC/aicharcards/the-game-master</tt></li>
|
||||
<li>Direct PNG Link (refer to <code>config.yaml</code> for allowed hosts)<br>Example: <tt>https://files.catbox.moe/notarealfile.png</tt></li>
|
||||
<ul>`;
|
||||
const input = await callPopup(html, 'input', '', { okButton: 'Import', rows: 4 });
|
||||
const input = await callGenericPopup(html, POPUP_TYPE.INPUT, '', { okButton: 'Import', rows: 4 });
|
||||
|
||||
if (!input) {
|
||||
console.debug('Custom content import cancelled');
|
||||
|
@ -10329,6 +10530,11 @@ jQuery(async function () {
|
|||
showMoreMessages();
|
||||
});
|
||||
|
||||
$(document).on('click', '.open_characters_library', async function () {
|
||||
await getCharacters();
|
||||
eventSource.emit(event_types.OPEN_CHARACTER_LIBRARY);
|
||||
});
|
||||
|
||||
// Added here to prevent execution before script.js is loaded and get rid of quirky timeouts
|
||||
await firstLoadInit();
|
||||
|
||||
|
|
|
@ -424,7 +424,7 @@ function restoreUserInput() {
|
|||
|
||||
const userInput = LoadLocal('userInput');
|
||||
if (userInput) {
|
||||
$('#send_textarea').val(userInput).trigger('input');
|
||||
$('#send_textarea').val(userInput)[0].dispatchEvent(new Event('input', { bubbles:true }));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -702,12 +702,12 @@ const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
|
|||
*/
|
||||
function autoFitSendTextArea() {
|
||||
const originalScrollBottom = chatBlock.scrollHeight - (chatBlock.scrollTop + chatBlock.offsetHeight);
|
||||
if (sendTextArea.scrollHeight == 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;
|
||||
}
|
||||
sendTextArea.style.height = sendTextArea.scrollHeight + 0.3 + 'px';
|
||||
sendTextArea.style.height = sendTextArea.scrollHeight + 3 + 'px';
|
||||
|
||||
if (!isFirefox) {
|
||||
const newScrollTop = Math.round(chatBlock.scrollHeight - (chatBlock.offsetHeight + originalScrollBottom));
|
||||
|
@ -1133,6 +1133,11 @@ export function initRossMods() {
|
|||
return;
|
||||
}
|
||||
|
||||
if ($('#dialogue_del_mes_cancel').is(':visible')) {
|
||||
$('#dialogue_del_mes_cancel').trigger('click');
|
||||
return;
|
||||
}
|
||||
|
||||
if ($('.drawer-content')
|
||||
.not('#WorldInfo')
|
||||
.not('#left-nav-panel')
|
||||
|
|
|
@ -9,10 +9,12 @@ import {
|
|||
} from '../script.js';
|
||||
import { selected_group } from './group-chats.js';
|
||||
import { extension_settings, getContext, saveMetadataDebounced } from './extensions.js';
|
||||
import { registerSlashCommand } from './slash-commands.js';
|
||||
import { getCharaFilename, debounce, delay } from './utils.js';
|
||||
import { getTokenCountAsync } from './tokenizers.js';
|
||||
import { debounce_timeout } from './constants.js';
|
||||
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
|
||||
import { SlashCommand } from './slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';
|
||||
export { MODULE_NAME as NOTE_MODULE_NAME };
|
||||
|
||||
const MODULE_NAME = '2_floating_prompt'; // <= Deliberate, for sorting lower than memory
|
||||
|
@ -455,9 +457,59 @@ export function initAuthorsNote() {
|
|||
});
|
||||
$('#option_toggle_AN').on('click', onANMenuItemClick);
|
||||
|
||||
registerSlashCommand('note', setNoteTextCommand, [], '<span class=\'monospace\'>(text)</span> – sets an author\'s note for the currently selected chat', true, true);
|
||||
registerSlashCommand('depth', setNoteDepthCommand, [], '<span class=\'monospace\'>(number)</span> – sets an author\'s note depth for in-chat positioning', true, true);
|
||||
registerSlashCommand('freq', setNoteIntervalCommand, ['interval'], '<span class=\'monospace\'>(number)</span> – sets an author\'s note insertion frequency', true, true);
|
||||
registerSlashCommand('pos', setNotePositionCommand, ['position'], '(<span class=\'monospace\'>chat</span> or <span class=\'monospace\'>scenario</span>) – sets an author\'s note position', true, true);
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'note',
|
||||
callback: setNoteTextCommand,
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'text', [ARGUMENT_TYPE.STRING], true,
|
||||
),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Sets an author's note for the currently selected chat.
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'depth',
|
||||
callback: setNoteDepthCommand,
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'number', [ARGUMENT_TYPE.NUMBER], true,
|
||||
),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Sets an author's note depth for in-chat positioning.
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'freq',
|
||||
callback: setNoteIntervalCommand,
|
||||
namedArgumentList: [],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'number', [ARGUMENT_TYPE.NUMBER], true,
|
||||
),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Sets an author's note insertion frequency.
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'pos',
|
||||
callback: setNotePositionCommand,
|
||||
namedArgumentList: [],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'position', [ARGUMENT_TYPE.STRING], true, false, null, ['chat', 'scenario'],
|
||||
),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Sets an author's note position.
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,792 @@
|
|||
import { power_user } from '../power-user.js';
|
||||
import { debounce, escapeRegex } from '../utils.js';
|
||||
import { AutoCompleteOption } from './AutoCompleteOption.js';
|
||||
import { AutoCompleteFuzzyScore } from './AutoCompleteFuzzyScore.js';
|
||||
import { BlankAutoCompleteOption } from './BlankAutoCompleteOption.js';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { AutoCompleteNameResult } from './AutoCompleteNameResult.js';
|
||||
import { AutoCompleteSecondaryNameResult } from './AutoCompleteSecondaryNameResult.js';
|
||||
|
||||
/**@readonly*/
|
||||
/**@enum {Number}*/
|
||||
export const AUTOCOMPLETE_WIDTH = {
|
||||
'INPUT': 0,
|
||||
'CHAT': 1,
|
||||
'FULL': 2,
|
||||
};
|
||||
|
||||
export class AutoComplete {
|
||||
/**@type {HTMLTextAreaElement}*/ textarea;
|
||||
/**@type {boolean}*/ isFloating = false;
|
||||
/**@type {()=>boolean}*/ checkIfActivate;
|
||||
/**@type {(text:string, index:number) => Promise<AutoCompleteNameResult>}*/ getNameAt;
|
||||
|
||||
/**@type {boolean}*/ isActive = false;
|
||||
/**@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;
|
||||
/**@type {AutoCompleteSecondaryNameResult}*/ secondaryParserResult;
|
||||
get effectiveParserResult() { return this.secondaryParserResult ?? this.parserResult; }
|
||||
/**@type {string}*/ name;
|
||||
|
||||
/**@type {boolean}*/ startQuote;
|
||||
/**@type {boolean}*/ endQuote;
|
||||
/**@type {number}*/ selectionStart;
|
||||
|
||||
/**@type {RegExp}*/ fuzzyRegex;
|
||||
|
||||
/**@type {AutoCompleteOption[]}*/ result = [];
|
||||
/**@type {AutoCompleteOption}*/ selectedItem = null;
|
||||
|
||||
/**@type {HTMLElement}*/ clone;
|
||||
/**@type {HTMLElement}*/ domWrap;
|
||||
/**@type {HTMLElement}*/ dom;
|
||||
/**@type {HTMLElement}*/ detailsWrap;
|
||||
/**@type {HTMLElement}*/ detailsDom;
|
||||
|
||||
/**@type {function}*/ renderDebounced;
|
||||
/**@type {function}*/ renderDetailsDebounced;
|
||||
/**@type {function}*/ updatePositionDebounced;
|
||||
/**@type {function}*/ updateDetailsPositionDebounced;
|
||||
/**@type {function}*/ updateFloatingPositionDebounced;
|
||||
|
||||
get matchType() {
|
||||
return power_user.stscript.matching ?? 'fuzzy';
|
||||
}
|
||||
|
||||
get autoHide() {
|
||||
return power_user.stscript.autocomplete.autoHide ?? false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @param {HTMLTextAreaElement} textarea The textarea to receive autocomplete.
|
||||
* @param {() => boolean} checkIfActivate Function should return true only if under the current conditions, autocomplete should display (e.g., for slash commands: autoComplete.text[0] == '/')
|
||||
* @param {(text: string, index: number) => Promise<AutoCompleteNameResult>} getNameAt Function should return (unfiltered, matching against input is done in AutoComplete) information about name options at index in text.
|
||||
* @param {boolean} isFloating Whether autocomplete should float at the keyboard cursor.
|
||||
*/
|
||||
constructor(textarea, checkIfActivate, getNameAt, isFloating = false) {
|
||||
this.textarea = textarea;
|
||||
this.checkIfActivate = checkIfActivate;
|
||||
this.getNameAt = getNameAt;
|
||||
this.isFloating = isFloating;
|
||||
|
||||
this.domWrap = document.createElement('div'); {
|
||||
this.domWrap.classList.add('autoComplete-wrap');
|
||||
if (isFloating) this.domWrap.classList.add('isFloating');
|
||||
}
|
||||
this.dom = document.createElement('ul'); {
|
||||
this.dom.classList.add('autoComplete');
|
||||
this.domWrap.append(this.dom);
|
||||
}
|
||||
this.detailsWrap = document.createElement('div'); {
|
||||
this.detailsWrap.classList.add('autoComplete-detailsWrap');
|
||||
if (isFloating) this.detailsWrap.classList.add('isFloating');
|
||||
}
|
||||
this.detailsDom = document.createElement('div'); {
|
||||
this.detailsDom.classList.add('autoComplete-details');
|
||||
this.detailsWrap.append(this.detailsDom);
|
||||
}
|
||||
|
||||
this.renderDebounced = debounce(this.render.bind(this), 10);
|
||||
this.renderDetailsDebounced = debounce(this.renderDetails.bind(this), 10);
|
||||
this.updatePositionDebounced = debounce(this.updatePosition.bind(this), 10);
|
||||
this.updateDetailsPositionDebounced = debounce(this.updateDetailsPosition.bind(this), 10);
|
||||
this.updateFloatingPositionDebounced = debounce(this.updateFloatingPosition.bind(this), 10);
|
||||
|
||||
textarea.addEventListener('input', ()=>this.text != this.textarea.value && this.show(true, this.wasForced));
|
||||
textarea.addEventListener('keydown', (evt)=>this.handleKeyDown(evt));
|
||||
textarea.addEventListener('click', ()=>this.isActive ? this.show() : null);
|
||||
textarea.addEventListener('selectionchange', ()=>this.show());
|
||||
textarea.addEventListener('blur', ()=>this.hide());
|
||||
if (isFloating) {
|
||||
textarea.addEventListener('scroll', ()=>this.updateFloatingPositionDebounced());
|
||||
}
|
||||
window.addEventListener('resize', ()=>this.updatePositionDebounced());
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {AutoCompleteOption} option
|
||||
*/
|
||||
makeItem(option) {
|
||||
const li = option.renderItem();
|
||||
// gotta listen to pointerdown (happens before textarea-blur)
|
||||
li.addEventListener('pointerdown', (evt)=>{
|
||||
evt.preventDefault();
|
||||
this.selectedItem = this.result.find(it=>it.name == li.getAttribute('data-name'));
|
||||
this.select();
|
||||
});
|
||||
return li;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {AutoCompleteOption} item
|
||||
*/
|
||||
updateName(item) {
|
||||
const chars = Array.from(item.dom.querySelector('.name').children);
|
||||
switch (this.matchType) {
|
||||
case 'strict': {
|
||||
chars.forEach((it, idx)=>{
|
||||
if (idx + item.nameOffset < item.name.length) {
|
||||
it.classList.add('matched');
|
||||
} else {
|
||||
it.classList.remove('matched');
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'includes': {
|
||||
const start = item.name.toLowerCase().search(this.name);
|
||||
chars.forEach((it, idx)=>{
|
||||
if (idx + item.nameOffset < start) {
|
||||
it.classList.remove('matched');
|
||||
} else if (idx + item.nameOffset < start + item.name.length) {
|
||||
it.classList.add('matched');
|
||||
} else {
|
||||
it.classList.remove('matched');
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'fuzzy': {
|
||||
item.name.replace(this.fuzzyRegex, (_, ...parts)=>{
|
||||
parts.splice(-2, 2);
|
||||
if (parts.length == 2) {
|
||||
chars.forEach(c=>c.classList.remove('matched'));
|
||||
} else {
|
||||
let cIdx = item.nameOffset;
|
||||
parts.forEach((it, idx)=>{
|
||||
if (it === null || it.length == 0) return '';
|
||||
if (idx % 2 == 1) {
|
||||
chars.slice(cIdx, cIdx + it.length).forEach(c=>c.classList.add('matched'));
|
||||
} else {
|
||||
chars.slice(cIdx, cIdx + it.length).forEach(c=>c.classList.remove('matched'));
|
||||
}
|
||||
cIdx += it.length;
|
||||
});
|
||||
}
|
||||
return '';
|
||||
});
|
||||
}
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate score for the fuzzy match.
|
||||
* @param {AutoCompleteOption} option
|
||||
* @returns The option.
|
||||
*/
|
||||
fuzzyScore(option) {
|
||||
const parts = this.fuzzyRegex.exec(option.name).slice(1, -1);
|
||||
let start = null;
|
||||
let consecutive = [];
|
||||
let current = '';
|
||||
let offset = 0;
|
||||
parts.forEach((part, idx) => {
|
||||
if (idx % 2 == 0) {
|
||||
if (part.length > 0) {
|
||||
if (current.length > 0) {
|
||||
consecutive.push(current);
|
||||
}
|
||||
current = '';
|
||||
}
|
||||
} else {
|
||||
if (start === null) {
|
||||
start = offset;
|
||||
}
|
||||
current += part;
|
||||
}
|
||||
offset += part.length;
|
||||
});
|
||||
if (current.length > 0) {
|
||||
consecutive.push(current);
|
||||
}
|
||||
consecutive.sort((a,b)=>b.length - a.length);
|
||||
option.score = new AutoCompleteFuzzyScore(start, consecutive[0]?.length ?? 0);
|
||||
return option;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two auto complete options by their fuzzy score.
|
||||
* @param {AutoCompleteOption} a
|
||||
* @param {AutoCompleteOption} b
|
||||
*/
|
||||
fuzzyScoreCompare(a, b) {
|
||||
if (a.score.start < b.score.start) return -1;
|
||||
if (a.score.start > b.score.start) return 1;
|
||||
if (a.score.longestConsecutive > b.score.longestConsecutive) return -1;
|
||||
if (a.score.longestConsecutive < b.score.longestConsecutive) return 1;
|
||||
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.
|
||||
* @param {boolean} isForced Whether force-showing (ctrl+space).
|
||||
* @param {boolean} isSelect Whether an autocomplete option was just selected.
|
||||
*/
|
||||
async show(isInput = false, isForced = false, isSelect = false) {
|
||||
//TODO check if isInput and isForced are both required
|
||||
this.text = this.textarea.value;
|
||||
this.isReplaceable = false;
|
||||
|
||||
if (document.activeElement != this.textarea) {
|
||||
// only show with textarea in focus
|
||||
return this.hide();
|
||||
}
|
||||
if (!this.checkIfActivate()) {
|
||||
// only show if provider wants to
|
||||
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);
|
||||
this.secondaryParserResult = null;
|
||||
|
||||
if (!this.parserResult) {
|
||||
// don't show if no name result found, e.g., cursor's area is not a command
|
||||
return this.hide();
|
||||
}
|
||||
|
||||
// need to know if name can be inside quotes, and then check if quotes are already there
|
||||
if (this.parserResult.canBeQuoted) {
|
||||
this.startQuote = this.text[this.parserResult.start] == '"';
|
||||
this.endQuote = this.startQuote && this.text[this.parserResult.start + this.parserResult.name.length + 1] == '"';
|
||||
} else {
|
||||
this.startQuote = false;
|
||||
this.endQuote = false;
|
||||
}
|
||||
|
||||
// use lowercase name for matching
|
||||
this.name = this.parserResult.name.toLowerCase() ?? '';
|
||||
|
||||
const isCursorInNamePart = this.textarea.selectionStart >= this.parserResult.start && this.textarea.selectionStart <= this.parserResult.start + this.parserResult.name.length + (this.startQuote ? 1 : 0);
|
||||
if (isForced || isInput) {
|
||||
// if forced (ctrl+space) or user input...
|
||||
if (isCursorInNamePart) {
|
||||
// ...and cursor is somewhere in the name part (including right behind the final char)
|
||||
// -> show autocomplete for the (partial if cursor in the middle) name
|
||||
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) {
|
||||
// is forced or user input or just selected autocomplete option...
|
||||
if (!isCursorInNamePart) {
|
||||
// ...and cursor is not somwehere in the main name part -> check for secondary options (e.g., named arguments)
|
||||
const result = this.parserResult.getSecondaryNameAt(this.text, this.textarea.selectionStart, isSelect);
|
||||
if (result && (isForced || result.isRequired)) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.matchType == 'fuzzy') {
|
||||
// only build the fuzzy regex if match type is set to fuzzy
|
||||
this.fuzzyRegex = new RegExp(`^(.*?)${this.name.split('').map(char=>`(${escapeRegex(char)})`).join('(.*?)')}(.*?)$`, 'i');
|
||||
}
|
||||
|
||||
//TODO maybe move the matchers somewhere else; a single match function? matchType is available as property
|
||||
const matchers = {
|
||||
'strict': (name) => name.toLowerCase().startsWith(this.name),
|
||||
'includes': (name) => name.toLowerCase().includes(this.name),
|
||||
'fuzzy': (name) => this.fuzzyRegex.test(name),
|
||||
};
|
||||
|
||||
this.result = this.effectiveParserResult.optionList
|
||||
// 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);
|
||||
|
||||
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
|
||||
option.dom = this.makeItem(option);
|
||||
// update replacer and add quotes if necessary
|
||||
if (this.effectiveParserResult.canBeQuoted) {
|
||||
option.replacer = option.name.includes(' ') || this.startQuote || this.endQuote ? `"${option.name}"` : `${option.name}`;
|
||||
} else {
|
||||
option.replacer = option.name;
|
||||
}
|
||||
// calculate fuzzy score if matching is fuzzy
|
||||
if (this.matchType == 'fuzzy') this.fuzzyScore(option);
|
||||
// update the name to highlight the matched chars
|
||||
this.updateName(option);
|
||||
return option;
|
||||
})
|
||||
// sort by fuzzy score or alphabetical
|
||||
.toSorted(this.matchType == 'fuzzy' ? this.fuzzyScoreCompare : (a, b) => a.name.localeCompare(b.name))
|
||||
;
|
||||
|
||||
|
||||
|
||||
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
|
||||
return this.hide();
|
||||
}
|
||||
// otherwise add "no match" notice
|
||||
const option = new BlankAutoCompleteOption(
|
||||
this.name.length ?
|
||||
this.effectiveParserResult.makeNoMatchText()
|
||||
: this.effectiveParserResult.makeNoOptionstext()
|
||||
,
|
||||
);
|
||||
this.result.push(option);
|
||||
} else if (this.result.length == 1 && this.effectiveParserResult && this.result[0].name == this.effectiveParserResult.name) {
|
||||
// only one result that is exactly the current value? just show hint, no autocomplete
|
||||
this.isReplaceable = false;
|
||||
this.isShowingDetails = false;
|
||||
} else if (!this.isReplaceable && this.result.length > 1) {
|
||||
return this.hide();
|
||||
}
|
||||
this.selectedItem = this.result[0];
|
||||
this.isActive = true;
|
||||
this.wasForced = isForced;
|
||||
this.renderDebounced();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide autocomplete.
|
||||
*/
|
||||
hide() {
|
||||
this.domWrap?.remove();
|
||||
this.detailsWrap?.remove();
|
||||
this.isActive = false;
|
||||
this.isShowingDetails = false;
|
||||
this.wasForced = false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Create updated DOM.
|
||||
*/
|
||||
render() {
|
||||
if (!this.isActive) return this.domWrap.remove();
|
||||
if (this.isReplaceable) {
|
||||
this.dom.innerHTML = '';
|
||||
const frag = document.createDocumentFragment();
|
||||
for (const item of this.result) {
|
||||
if (item == this.selectedItem) {
|
||||
item.dom.classList.add('selected');
|
||||
} else {
|
||||
item.dom.classList.remove('selected');
|
||||
}
|
||||
frag.append(item.dom);
|
||||
}
|
||||
this.dom.append(frag);
|
||||
this.updatePosition();
|
||||
document.body.append(this.domWrap);
|
||||
} else {
|
||||
this.domWrap.remove();
|
||||
}
|
||||
this.renderDetailsDebounced();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create updated DOM for details.
|
||||
*/
|
||||
renderDetails() {
|
||||
if (!this.isActive) return this.detailsWrap.remove();
|
||||
if (!this.isShowingDetails && this.isReplaceable) return this.detailsWrap.remove();
|
||||
this.detailsDom.innerHTML = '';
|
||||
this.detailsDom.append(this.selectedItem?.renderDetails() ?? 'NO ITEM');
|
||||
document.body.append(this.detailsWrap);
|
||||
this.updateDetailsPositionDebounced();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Update position of DOM.
|
||||
*/
|
||||
updatePosition() {
|
||||
if (this.isFloating) {
|
||||
this.updateFloatingPosition();
|
||||
} else {
|
||||
const rect = {};
|
||||
rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect();
|
||||
rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect();
|
||||
rect[AUTOCOMPLETE_WIDTH.FULL] = document.body.getBoundingClientRect();
|
||||
this.domWrap.style.setProperty('--bottom', `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`);
|
||||
this.dom.style.setProperty('--bottom', `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`);
|
||||
this.domWrap.style.bottom = `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`;
|
||||
if (this.isShowingDetails) {
|
||||
this.domWrap.style.setProperty('--leftOffset', '1vw');
|
||||
this.domWrap.style.setProperty('--leftOffset', `max(1vw, ${rect[power_user.stscript.autocomplete.width.left].left}px)`);
|
||||
this.domWrap.style.setProperty('--rightOffset', `calc(100vw - min(${rect[power_user.stscript.autocomplete.width.right].right}px, ${this.isShowingDetails ? 74 : 0}vw)`);
|
||||
} else {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update position of details DOM.
|
||||
*/
|
||||
updateDetailsPosition() {
|
||||
if (this.isShowingDetails || !this.isReplaceable) {
|
||||
if (this.isFloating) {
|
||||
this.updateFloatingDetailsPosition();
|
||||
} else {
|
||||
const rect = {};
|
||||
rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect();
|
||||
rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect();
|
||||
rect[AUTOCOMPLETE_WIDTH.FULL] = document.body.getBoundingClientRect();
|
||||
if (this.isReplaceable) {
|
||||
this.detailsWrap.classList.remove('full');
|
||||
const selRect = this.selectedItem.dom.children[0].getBoundingClientRect();
|
||||
this.detailsWrap.style.setProperty('--targetOffset', `${selRect.top}`);
|
||||
this.detailsWrap.style.setProperty('--rightOffset', '1vw');
|
||||
this.detailsWrap.style.setProperty('--bottomOffset', `calc(100vh - ${rect[AUTOCOMPLETE_WIDTH.INPUT].top}px)`);
|
||||
this.detailsWrap.style.setProperty('--leftOffset', `calc(100vw - ${this.domWrap.style.getPropertyValue('--rightOffset')}`);
|
||||
} else {
|
||||
this.detailsWrap.classList.add('full');
|
||||
this.detailsWrap.style.setProperty('--targetOffset', `${rect[AUTOCOMPLETE_WIDTH.INPUT].top}`);
|
||||
this.detailsWrap.style.setProperty('--bottomOffset', `calc(100vh - ${rect[AUTOCOMPLETE_WIDTH.INPUT].top}px)`);
|
||||
this.detailsWrap.style.setProperty('--leftOffset', `${rect[power_user.stscript.autocomplete.width.left].left}px`);
|
||||
this.detailsWrap.style.setProperty('--rightOffset', `calc(100vw - ${rect[power_user.stscript.autocomplete.width.right].right}px)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update position of floating autocomplete.
|
||||
*/
|
||||
updateFloatingPosition() {
|
||||
const location = this.getCursorPosition();
|
||||
const rect = this.textarea.getBoundingClientRect();
|
||||
// cursor is out of view -> hide
|
||||
if (location.bottom < rect.top || location.top > rect.bottom || location.left < rect.left || location.left > rect.right) {
|
||||
return this.hide();
|
||||
}
|
||||
const left = Math.max(rect.left, location.left);
|
||||
this.domWrap.style.setProperty('--targetOffset', `${left}`);
|
||||
if (location.top <= window.innerHeight / 2) {
|
||||
// if cursor is in lower half of window, show list above line
|
||||
this.domWrap.style.top = `${location.bottom}px`;
|
||||
this.domWrap.style.bottom = 'auto';
|
||||
this.domWrap.style.maxHeight = `calc(${location.bottom}px - 1vh)`;
|
||||
} else {
|
||||
// if cursor is in upper half of window, show list below line
|
||||
this.domWrap.style.top = 'auto';
|
||||
this.domWrap.style.bottom = `calc(100vh - ${location.top}px)`;
|
||||
this.domWrap.style.maxHeight = `calc(${location.top}px - 1vh)`;
|
||||
}
|
||||
}
|
||||
|
||||
updateFloatingDetailsPosition(location = null) {
|
||||
if (!location) location = this.getCursorPosition();
|
||||
const rect = this.textarea.getBoundingClientRect();
|
||||
if (location.bottom < rect.top || location.top > rect.bottom || location.left < rect.left || location.left > rect.right) {
|
||||
return this.hide();
|
||||
}
|
||||
const left = Math.max(rect.left, location.left);
|
||||
this.detailsWrap.style.setProperty('--targetOffset', `${left}`);
|
||||
if (this.isReplaceable) {
|
||||
this.detailsWrap.classList.remove('full');
|
||||
if (left < window.innerWidth / 4) {
|
||||
// if cursor is in left part of screen, show details on right of list
|
||||
this.detailsWrap.classList.add('right');
|
||||
this.detailsWrap.classList.remove('left');
|
||||
} else {
|
||||
// if cursor is in right part of screen, show details on left of list
|
||||
this.detailsWrap.classList.remove('right');
|
||||
this.detailsWrap.classList.add('left');
|
||||
}
|
||||
} else {
|
||||
this.detailsWrap.classList.remove('left');
|
||||
this.detailsWrap.classList.remove('right');
|
||||
this.detailsWrap.classList.add('full');
|
||||
}
|
||||
if (location.top <= window.innerHeight / 2) {
|
||||
// if cursor is in lower half of window, show list above line
|
||||
this.detailsWrap.style.top = `${location.bottom}px`;
|
||||
this.detailsWrap.style.bottom = 'auto';
|
||||
this.detailsWrap.style.maxHeight = `calc(${location.bottom}px - 1vh)`;
|
||||
} else {
|
||||
// if cursor is in upper half of window, show list below line
|
||||
this.detailsWrap.style.top = 'auto';
|
||||
this.detailsWrap.style.bottom = `calc(100vh - ${location.top}px)`;
|
||||
this.detailsWrap.style.maxHeight = `calc(${location.top}px - 1vh)`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate (keyboard) cursor coordinates within textarea.
|
||||
* @returns {{left:number, top:number, bottom:number}}
|
||||
*/
|
||||
getCursorPosition() {
|
||||
const inputRect = this.textarea.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(this.textarea);
|
||||
if (!this.clone) {
|
||||
this.clone = document.createElement('div');
|
||||
for (const key of style) {
|
||||
this.clone.style[key] = style[key];
|
||||
}
|
||||
this.clone.style.position = 'fixed';
|
||||
this.clone.style.visibility = 'hidden';
|
||||
document.body.append(this.clone);
|
||||
const mo = new MutationObserver(muts=>{
|
||||
if (muts.find(it=>Array.from(it.removedNodes).includes(this.textarea))) {
|
||||
this.clone.remove();
|
||||
}
|
||||
});
|
||||
mo.observe(this.textarea.parentElement, { childList:true });
|
||||
}
|
||||
this.clone.style.height = `${inputRect.height}px`;
|
||||
this.clone.style.left = `${inputRect.left}px`;
|
||||
this.clone.style.top = `${inputRect.top}px`;
|
||||
this.clone.style.whiteSpace = style.whiteSpace;
|
||||
this.clone.style.tabSize = style.tabSize;
|
||||
const text = this.textarea.value;
|
||||
const before = text.slice(0, this.textarea.selectionStart);
|
||||
this.clone.textContent = before;
|
||||
const locator = document.createElement('span');
|
||||
locator.textContent = text[this.textarea.selectionStart];
|
||||
this.clone.append(locator);
|
||||
this.clone.append(text.slice(this.textarea.selectionStart + 1));
|
||||
this.clone.scrollTop = this.textarea.scrollTop;
|
||||
this.clone.scrollLeft = this.textarea.scrollLeft;
|
||||
const locatorRect = locator.getBoundingClientRect();
|
||||
const location = {
|
||||
left: locatorRect.left,
|
||||
top: locatorRect.top,
|
||||
bottom: locatorRect.bottom,
|
||||
};
|
||||
return location;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Toggle details view alongside autocomplete list.
|
||||
*/
|
||||
toggleDetails() {
|
||||
this.isShowingDetails = !this.isShowingDetails;
|
||||
this.renderDetailsDebounced();
|
||||
this.updatePosition();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Select an item for autocomplete and put text into textarea.
|
||||
*/
|
||||
async select() {
|
||||
if (this.isReplaceable && this.selectedItem.value !== null) {
|
||||
this.textarea.value = `${this.text.slice(0, this.effectiveParserResult.start)}${this.selectedItem.replacer}${this.text.slice(this.effectiveParserResult.start + this.effectiveParserResult.name.length + (this.startQuote ? 1 : 0) + (this.endQuote ? 1 : 0))}`;
|
||||
this.textarea.selectionStart = this.effectiveParserResult.start + this.selectedItem.replacer.length;
|
||||
this.textarea.selectionEnd = this.textarea.selectionStart;
|
||||
this.show(false, false, true);
|
||||
} else {
|
||||
const selectionStart = this.textarea.selectionStart;
|
||||
const selectionEnd = this.textarea.selectionDirection;
|
||||
this.textarea.selectionStart = selectionStart;
|
||||
this.textarea.selectionDirection = selectionEnd;
|
||||
}
|
||||
this.wasForced = false;
|
||||
this.textarea.dispatchEvent(new Event('input', { bubbles:true }));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Mark the item at newIdx in the autocomplete list as selected.
|
||||
* @param {number} newIdx
|
||||
*/
|
||||
selectItemAtIndex(newIdx) {
|
||||
this.selectedItem.dom.classList.remove('selected');
|
||||
this.selectedItem = this.result[newIdx];
|
||||
this.selectedItem.dom.classList.add('selected');
|
||||
const rect = this.selectedItem.dom.children[0].getBoundingClientRect();
|
||||
const rectParent = this.dom.getBoundingClientRect();
|
||||
if (rect.top < rectParent.top || rect.bottom > rectParent.bottom ) {
|
||||
this.dom.scrollTop += rect.top < rectParent.top ? rect.top - rectParent.top : rect.bottom - rectParent.bottom;
|
||||
}
|
||||
this.renderDetailsDebounced();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle keyboard events.
|
||||
* @param {KeyboardEvent} evt The event.
|
||||
*/
|
||||
async handleKeyDown(evt) {
|
||||
// autocomplete is shown and cursor at end of current command name (or inside name and typed or forced)
|
||||
if (this.isActive && this.isReplaceable) {
|
||||
// actions in the list
|
||||
switch (evt.key) {
|
||||
case 'ArrowUp': {
|
||||
// select previous item
|
||||
if (evt.ctrlKey || evt.altKey || evt.shiftKey) return;
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
const idx = this.result.indexOf(this.selectedItem);
|
||||
let newIdx;
|
||||
if (idx == 0) newIdx = this.result.length - 1;
|
||||
else newIdx = idx - 1;
|
||||
this.selectItemAtIndex(newIdx);
|
||||
return;
|
||||
}
|
||||
case 'ArrowDown': {
|
||||
// select next item
|
||||
if (evt.ctrlKey || evt.altKey || evt.shiftKey) return;
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
const idx = this.result.indexOf(this.selectedItem);
|
||||
const newIdx = (idx + 1) % this.result.length;
|
||||
this.selectItemAtIndex(newIdx);
|
||||
return;
|
||||
}
|
||||
case 'Enter': {
|
||||
// pick the selected item to autocomplete
|
||||
if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.value == '') break;
|
||||
if (this.selectedItem.name == this.name) break;
|
||||
evt.preventDefault();
|
||||
evt.stopImmediatePropagation();
|
||||
this.select();
|
||||
return;
|
||||
}
|
||||
case 'Tab': {
|
||||
// pick the selected item to autocomplete
|
||||
if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.value == '') break;
|
||||
evt.preventDefault();
|
||||
evt.stopImmediatePropagation();
|
||||
this.select();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// details are shown, cursor can be anywhere
|
||||
if (this.isActive) {
|
||||
switch (evt.key) {
|
||||
case 'Escape': {
|
||||
// close autocomplete
|
||||
if (evt.ctrlKey || evt.altKey || evt.shiftKey) return;
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
this.isForceHidden = true;
|
||||
this.wasForced = false;
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
case 'Enter': {
|
||||
// hide autocomplete on enter (send, execute, ...)
|
||||
if (!evt.shiftKey) {
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// autocomplete shown or not, cursor anywhere
|
||||
switch (evt.key) {
|
||||
case ' ': {
|
||||
if (evt.ctrlKey) {
|
||||
if (this.isActive && this.isReplaceable) {
|
||||
// ctrl-space to toggle details for selected item
|
||||
this.toggleDetails();
|
||||
} else {
|
||||
// ctrl-space to force show autocomplete
|
||||
this.show(false, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (['Control', 'Shift', 'Alt'].includes(evt.key)) {
|
||||
// ignore keydown on modifier keys
|
||||
return;
|
||||
}
|
||||
switch (evt.key) {
|
||||
case 'ArrowUp':
|
||||
case 'ArrowDown':
|
||||
case 'ArrowRight':
|
||||
case 'ArrowLeft': {
|
||||
if (this.isActive) {
|
||||
// keyboard navigation, wait for keyup to complete cursor move
|
||||
const oldText = this.textarea.value;
|
||||
await new Promise(resolve=>{
|
||||
window.addEventListener('keyup', resolve, { once:true });
|
||||
});
|
||||
if (this.selectionStart != this.textarea.selectionStart) {
|
||||
this.selectionStart = this.textarea.selectionStart;
|
||||
this.show(this.isReplaceable || oldText != this.textarea.value);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (this.isActive) {
|
||||
this.text != this.textarea.value && this.show(this.isReplaceable);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
|
||||
|
||||
|
||||
export class AutoCompleteFuzzyScore {
|
||||
/**@type {number}*/ start;
|
||||
/**@type {number}*/ longestConsecutive;
|
||||
|
||||
/**
|
||||
* @param {number} start
|
||||
* @param {number} longestConsecutive
|
||||
*/
|
||||
constructor(start, longestConsecutive) {
|
||||
this.start = start;
|
||||
this.longestConsecutive = longestConsecutive;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import { SlashCommandNamedArgumentAutoCompleteOption } from '../slash-commands/SlashCommandNamedArgumentAutoCompleteOption.js';
|
||||
import { AutoCompleteOption } from './AutoCompleteOption.js';
|
||||
// import { AutoCompleteSecondaryNameResult } from './AutoCompleteSecondaryNameResult.js';
|
||||
|
||||
|
||||
|
||||
export class AutoCompleteNameResult {
|
||||
/**@type {string} */ name;
|
||||
/**@type {number} */ start;
|
||||
/**@type {AutoCompleteOption[]} */ optionList = [];
|
||||
/**@type {boolean} */ canBeQuoted = false;
|
||||
/**@type {()=>string} */ makeNoMatchText = ()=>`No matches found for "${this.name}"`;
|
||||
/**@type {()=>string} */ makeNoOptionstext = ()=>'No options';
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} name Name (potentially partial) of the name at the requested index.
|
||||
* @param {number} start Index where the name starts.
|
||||
* @param {AutoCompleteOption[]} optionList A list of autocomplete options found in the current scope.
|
||||
* @param {boolean} canBeQuoted Whether the name can be inside quotes.
|
||||
* @param {()=>string} makeNoMatchText Function that returns text to show when no matches where found.
|
||||
* @param {()=>string} makeNoOptionsText Function that returns text to show when no options are available to match against.
|
||||
*/
|
||||
constructor(name, start, optionList = [], canBeQuoted = false, makeNoMatchText = null, makeNoOptionsText = null) {
|
||||
this.name = name;
|
||||
this.start = start;
|
||||
this.optionList = optionList;
|
||||
this.canBeQuoted = canBeQuoted;
|
||||
this.noMatchText = makeNoMatchText ?? this.makeNoMatchText;
|
||||
this.noOptionstext = makeNoOptionsText ?? this.makeNoOptionstext;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} text The whole text
|
||||
* @param {number} index Cursor index within text
|
||||
* @param {boolean} isSelect Whether autocomplete was triggered by selecting an autocomplete option
|
||||
* @returns {AutoCompleteSecondaryNameResult}
|
||||
*/
|
||||
getSecondaryNameAt(text, index, isSelect) {
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,206 @@
|
|||
import { SlashCommand } from '../slash-commands/SlashCommand.js';
|
||||
import { AutoCompleteFuzzyScore } from './AutoCompleteFuzzyScore.js';
|
||||
|
||||
|
||||
|
||||
export class AutoCompleteOption {
|
||||
/**@type {string}*/ name;
|
||||
/**@type {string}*/ typeIcon;
|
||||
/**@type {number}*/ nameOffset = 0;
|
||||
/**@type {AutoCompleteFuzzyScore}*/ score;
|
||||
/**@type {string}*/ replacer;
|
||||
/**@type {HTMLElement}*/ dom;
|
||||
|
||||
|
||||
/**
|
||||
* Used as a comparison value when removing duplicates (e.g., when a SlashCommand has aliases).
|
||||
* @type {any}
|
||||
* */
|
||||
get value() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
*/
|
||||
constructor(name, typeIcon = ' ') {
|
||||
this.name = name;
|
||||
this.typeIcon = typeIcon;
|
||||
}
|
||||
|
||||
|
||||
makeItem(key, typeIcon, noSlash, namedArguments = [], unnamedArguments = [], returnType = 'void', helpString = '', aliasList = []) {
|
||||
const li = document.createElement('li'); {
|
||||
li.classList.add('item');
|
||||
const type = document.createElement('span'); {
|
||||
type.classList.add('type');
|
||||
type.classList.add('monospace');
|
||||
type.textContent = typeIcon;
|
||||
li.append(type);
|
||||
}
|
||||
const specs = document.createElement('span'); {
|
||||
specs.classList.add('specs');
|
||||
const name = document.createElement('span'); {
|
||||
name.classList.add('name');
|
||||
name.classList.add('monospace');
|
||||
name.textContent = noSlash ? '' : '/';
|
||||
key.split('').forEach(char=>{
|
||||
const span = document.createElement('span'); {
|
||||
span.textContent = char;
|
||||
name.append(span);
|
||||
}
|
||||
});
|
||||
specs.append(name);
|
||||
}
|
||||
const body = document.createElement('span'); {
|
||||
body.classList.add('body');
|
||||
const args = document.createElement('span'); {
|
||||
args.classList.add('arguments');
|
||||
for (const arg of namedArguments) {
|
||||
const argItem = document.createElement('span'); {
|
||||
argItem.classList.add('argument');
|
||||
argItem.classList.add('namedArgument');
|
||||
if (!arg.isRequired || (arg.defaultValue ?? false)) argItem.classList.add('optional');
|
||||
if (arg.acceptsMultiple) argItem.classList.add('multiple');
|
||||
const name = document.createElement('span'); {
|
||||
name.classList.add('argument-name');
|
||||
name.textContent = arg.name;
|
||||
argItem.append(name);
|
||||
}
|
||||
if (arg.enumList.length > 0) {
|
||||
const enums = document.createElement('span'); {
|
||||
enums.classList.add('argument-enums');
|
||||
for (const e of arg.enumList) {
|
||||
const enumItem = document.createElement('span'); {
|
||||
enumItem.classList.add('argument-enum');
|
||||
enumItem.textContent = e;
|
||||
enums.append(enumItem);
|
||||
}
|
||||
}
|
||||
argItem.append(enums);
|
||||
}
|
||||
} else {
|
||||
const types = document.createElement('span'); {
|
||||
types.classList.add('argument-types');
|
||||
for (const t of arg.typeList) {
|
||||
const type = document.createElement('span'); {
|
||||
type.classList.add('argument-type');
|
||||
type.textContent = t;
|
||||
types.append(type);
|
||||
}
|
||||
}
|
||||
argItem.append(types);
|
||||
}
|
||||
}
|
||||
args.append(argItem);
|
||||
}
|
||||
}
|
||||
for (const arg of unnamedArguments) {
|
||||
const argItem = document.createElement('span'); {
|
||||
argItem.classList.add('argument');
|
||||
argItem.classList.add('unnamedArgument');
|
||||
if (!arg.isRequired || (arg.defaultValue ?? false)) argItem.classList.add('optional');
|
||||
if (arg.acceptsMultiple) argItem.classList.add('multiple');
|
||||
if (arg.enumList.length > 0) {
|
||||
const enums = document.createElement('span'); {
|
||||
enums.classList.add('argument-enums');
|
||||
for (const e of arg.enumList) {
|
||||
const enumItem = document.createElement('span'); {
|
||||
enumItem.classList.add('argument-enum');
|
||||
enumItem.textContent = e;
|
||||
enums.append(enumItem);
|
||||
}
|
||||
}
|
||||
argItem.append(enums);
|
||||
}
|
||||
} else {
|
||||
const types = document.createElement('span'); {
|
||||
types.classList.add('argument-types');
|
||||
for (const t of arg.typeList) {
|
||||
const type = document.createElement('span'); {
|
||||
type.classList.add('argument-type');
|
||||
type.textContent = t;
|
||||
types.append(type);
|
||||
}
|
||||
}
|
||||
argItem.append(types);
|
||||
}
|
||||
}
|
||||
args.append(argItem);
|
||||
}
|
||||
}
|
||||
body.append(args);
|
||||
}
|
||||
const returns = document.createElement('span'); {
|
||||
returns.classList.add('returns');
|
||||
returns.textContent = returnType ?? 'void';
|
||||
// body.append(returns);
|
||||
}
|
||||
specs.append(body);
|
||||
}
|
||||
li.append(specs);
|
||||
}
|
||||
const help = document.createElement('span'); {
|
||||
help.classList.add('help');
|
||||
const content = document.createElement('span'); {
|
||||
content.classList.add('helpContent');
|
||||
content.innerHTML = helpString;
|
||||
const text = content.textContent;
|
||||
content.innerHTML = '';
|
||||
content.textContent = text;
|
||||
help.append(content);
|
||||
}
|
||||
li.append(help);
|
||||
}
|
||||
if (aliasList.length > 0) {
|
||||
const aliases = document.createElement('span'); {
|
||||
aliases.classList.add('aliases');
|
||||
aliases.append(' (alias: ');
|
||||
for (const aliasName of aliasList) {
|
||||
const alias = document.createElement('span'); {
|
||||
alias.classList.add('monospace');
|
||||
alias.textContent = `/${aliasName}`;
|
||||
aliases.append(alias);
|
||||
}
|
||||
}
|
||||
aliases.append(')');
|
||||
// li.append(aliases);
|
||||
}
|
||||
}
|
||||
}
|
||||
return li;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
renderItem() {
|
||||
// throw new Error(`${this.constructor.name}.renderItem() is not implemented`);
|
||||
let li;
|
||||
li = this.makeItem(this.name, this.typeIcon, true);
|
||||
li.setAttribute('data-name', this.name);
|
||||
return li;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @returns {DocumentFragment}
|
||||
*/
|
||||
renderDetails() {
|
||||
// throw new Error(`${this.constructor.name}.renderDetails() is not implemented`);
|
||||
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);
|
||||
}
|
||||
return frag;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import { AutoCompleteNameResult } from './AutoCompleteNameResult.js';
|
||||
|
||||
export class AutoCompleteSecondaryNameResult extends AutoCompleteNameResult {
|
||||
/**@type {boolean}*/ isRequired = false;
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
import { AutoCompleteOption } from './AutoCompleteOption.js';
|
||||
|
||||
export class BlankAutoCompleteOption extends AutoCompleteOption {
|
||||
/**
|
||||
* @param {string} name
|
||||
*/
|
||||
constructor(name) {
|
||||
super(name);
|
||||
this.dom = this.renderItem();
|
||||
}
|
||||
|
||||
get value() { return null; }
|
||||
|
||||
|
||||
renderItem() {
|
||||
const li = document.createElement('li'); {
|
||||
li.classList.add('item');
|
||||
li.classList.add('blank');
|
||||
li.textContent = this.name;
|
||||
}
|
||||
return li;
|
||||
}
|
||||
|
||||
|
||||
renderDetails() {
|
||||
const frag = document.createDocumentFragment();
|
||||
return frag;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import { AutoCompleteOption } from './AutoCompleteOption.js';
|
||||
|
||||
export class MacroAutoCompleteOption extends AutoCompleteOption {
|
||||
/**@type {string}*/ fullName;
|
||||
/**@type {string}*/ description;
|
||||
|
||||
|
||||
constructor(name, fullName, description) {
|
||||
super(name, '{}');
|
||||
this.fullName = fullName;
|
||||
this.description = description;
|
||||
this.nameOffset = 2;
|
||||
}
|
||||
|
||||
|
||||
renderItem() {
|
||||
let li;
|
||||
li = this.makeItem(`${this.fullName}`, '{}', true, [], [], null, this.description);
|
||||
li.setAttribute('data-name', this.name);
|
||||
li.setAttribute('data-option-type', 'macro');
|
||||
return li;
|
||||
}
|
||||
|
||||
|
||||
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.fullName;
|
||||
specs.append(name);
|
||||
}
|
||||
frag.append(specs);
|
||||
}
|
||||
const help = document.createElement('span'); {
|
||||
help.classList.add('help');
|
||||
help.innerHTML = this.description;
|
||||
frag.append(help);
|
||||
}
|
||||
return frag;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import { callPopup, chat_metadata, eventSource, event_types, generateQuietPrompt, getCurrentChatId, getRequestHeaders, getThumbnailUrl, saveSettingsDebounced } from '../script.js';
|
||||
import { saveMetadataDebounced } from './extensions.js';
|
||||
import { registerSlashCommand } from './slash-commands.js';
|
||||
import { SlashCommand } from './slash-commands/SlashCommand.js';
|
||||
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
|
||||
import { flashHighlight, stringFormat } from './utils.js';
|
||||
|
||||
const BG_METADATA_KEY = 'custom_background';
|
||||
|
@ -480,7 +481,20 @@ export function initBackgrounds() {
|
|||
$('#auto_background').on('click', autoBackgroundCommand);
|
||||
$('#add_bg_button').on('change', onBackgroundUploadSelected);
|
||||
$('#bg-filter').on('input', onBackgroundFilterInput);
|
||||
registerSlashCommand('lockbg', onLockBackgroundClick, ['bglock'], '– locks a background for the currently selected chat', true, true);
|
||||
registerSlashCommand('unlockbg', onUnlockBackgroundClick, ['bgunlock'], '– unlocks a background for the currently selected chat', true, true);
|
||||
registerSlashCommand('autobg', autoBackgroundCommand, ['bgauto'], '– automatically changes the background based on the chat context using the AI request prompt', true, true);
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'lockbg',
|
||||
callback: onLockBackgroundClick,
|
||||
aliases: ['bglock'],
|
||||
helpString: 'Locks a background for the currently selected chat',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'unlockbg',
|
||||
callback: onUnlockBackgroundClick,
|
||||
aliases: ['bgunlock'],
|
||||
helpString: 'Unlocks a background for the currently selected chat',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'autobg',
|
||||
callback: autoBackgroundCommand,
|
||||
aliases: ['bgauto'],
|
||||
helpString: 'Automatically changes the background based on the chat context using the AI request prompt',
|
||||
}));
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
/**
|
||||
* @typedef {object} v2DataWorldInfoEntry
|
||||
* @property {string[]} keys - An array of primary keys associated with the entry.
|
||||
* @property {string[]} secondary_keys - An array of secondary keys associated with the entry (optional).
|
||||
* @property {string} comment - A human-readable description or explanation for the entry.
|
||||
* @property {string} content - The main content or data associated with the entry.
|
||||
* @property {boolean} constant - Indicates if the entry's content is fixed and unchangeable.
|
||||
* @property {boolean} selective - Indicates if the entry's inclusion is controlled by specific conditions.
|
||||
* @property {number} insertion_order - Defines the order in which the entry is inserted during processing.
|
||||
* @property {boolean} enabled - Controls whether the entry is currently active and used.
|
||||
* @property {string} position - Specifies the location or context where the entry applies.
|
||||
* @property {v2DataWorldInfoEntryExtensionInfos} extensions - An object containing additional details for extensions associated with the entry.
|
||||
* @property {number} id - A unique identifier assigned to the entry.
|
||||
*/
|
||||
/**
|
||||
* @typedef {object} v2DataWorldInfoEntryExtensionInfos
|
||||
* @property {number} position - The order in which the extension is applied relative to other extensions.
|
||||
* @property {boolean} exclude_recursion - Prevents the extension from being applied recursively.
|
||||
* @property {number} probability - The chance (between 0 and 1) of the extension being applied.
|
||||
* @property {boolean} useProbability - Determines if the `probability` property is used.
|
||||
* @property {number} depth - The maximum level of nesting allowed for recursive application of the extension.
|
||||
* @property {number} selectiveLogic - Defines the logic used to determine if the extension is applied selectively.
|
||||
* @property {string} group - A category or grouping for the extension.
|
||||
* @property {boolean} group_override - Overrides any existing group assignment for the extension.
|
||||
* @property {number} group_weight - A value used for prioritizing extensions within the same group.
|
||||
* @property {boolean} prevent_recursion - Completely disallows recursive application of the extension.
|
||||
* @property {boolean} delay_until_recursion - Will only be checked during recursion.
|
||||
* @property {number} scan_depth - The maximum depth to search for matches when applying the extension.
|
||||
* @property {boolean} match_whole_words - Specifies if only entire words should be matched during extension application.
|
||||
* @property {boolean} use_group_scoring - Indicates if group weight is considered when selecting extensions.
|
||||
* @property {boolean} case_sensitive - Controls whether case sensitivity is applied during matching for the extension.
|
||||
* @property {string} automation_id - An identifier used for automation purposes related to the extension.
|
||||
* @property {number} role - The specific function or purpose of the extension.
|
||||
* @property {boolean} vectorized - Indicates if the extension is optimized for vectorized processing.
|
||||
* @property {number} display_index - The order in which the extension should be displayed for user interfaces.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} v2WorldInfoBook
|
||||
* @property {string} name - the name of the book
|
||||
* @property {v2DataWorldInfoEntry[]} entries - the entries of the book
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} v2CharData
|
||||
* @property {string} name - The character's name.
|
||||
* @property {string} description - A brief description of the character.
|
||||
* @property {string} character_version - The character's data version.
|
||||
* @property {string} personality - A short summary of the character's personality traits.
|
||||
* @property {string} scenario - A description of the character's background or setting.
|
||||
* @property {string} first_mes - The character's opening message in a conversation.
|
||||
* @property {string} mes_example - An example message demonstrating the character's conversation style.
|
||||
* @property {string} creator_notes - Internal notes or comments left by the character's creator.
|
||||
* @property {string[]} tags - A list of keywords or labels associated with the character.
|
||||
* @property {string} system_prompt - The system prompt used to interact with the character.
|
||||
* @property {string} post_history_instructions - Instructions for handling the character's conversation history.
|
||||
* @property {string} creator - The name of the person who created the character.
|
||||
* @property {string[]} alternate_greetings - Additional greeting messages the character can use.
|
||||
* @property {v2WorldInfoBook} character_book - Data about the character's world or story (if applicable).
|
||||
* @property {v2CharDataExtensionInfos} extensions - Additional details specific to the character.
|
||||
*/
|
||||
/**
|
||||
* @typedef {object} v2CharDataExtensionInfos
|
||||
* @property {number} talkativeness - A numerical value indicating the character's propensity to talk.
|
||||
* @property {boolean} fav - A flag indicating whether the character is a favorite.
|
||||
* @property {string} world - The fictional world or setting where the character exists (if applicable).
|
||||
* @property {object} depth_prompt - Prompts used to explore the character's depth and complexity.
|
||||
* @property {number} depth_prompt.depth - The level of detail or nuance targeted by the prompt.
|
||||
* @property {string} depth_prompt.prompt - The actual prompt text used for deeper character interaction.
|
||||
* @property {"system" | "user" | "assistant"} depth_prompt.role - The role the character takes on during the prompted interaction (system, user, or assistant).
|
||||
* // Non-standard extensions added by external tools
|
||||
* @property {string} [pygmalion_id] - The unique identifier assigned to the character by the Pygmalion.chat.
|
||||
* @property {string} [github_repo] - The gitHub repository associated with the character.
|
||||
* @property {string} [source_url] - The source URL associated with the character.
|
||||
* @property {{full_path: string}} [chub] - The Chub-specific data associated with the character.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} v1CharData
|
||||
* @property {string} name - the name of the character
|
||||
* @property {string} description - the description of the character
|
||||
* @property {string} personality - a short personality description of the character
|
||||
* @property {string} scenario - a scenario description of the character
|
||||
* @property {string} first_mes - the first message in the conversation
|
||||
* @property {string} mes_example - the example message in the conversation
|
||||
* @property {string} creatorcomment - creator's notes of the character
|
||||
* @property {string[]} tags - the tags of the character
|
||||
* @property {number} talkativeness - talkativeness
|
||||
* @property {boolean|string} fav - fav
|
||||
* @property {string} create_date - create_date
|
||||
* @property {v2CharData} data - v2 data extension
|
||||
* // Non-standard extensions added by the ST server (not part of the original data)
|
||||
* @property {string} chat - name of the current chat file chat
|
||||
* @property {string} avatar - file name of the avatar image (acts as a unique identifier)
|
||||
* @property {string} json_data - the full raw JSON data of the character
|
||||
*/
|
||||
export default 0;// now this file is a module
|
|
@ -0,0 +1,9 @@
|
|||
<div class="characterAsset">
|
||||
<div class="characterAssetName">{{name}}</div>
|
||||
<img class="characterAssetImage" alt="{{name}}" src="{{url}}" />
|
||||
<div class="characterAssetDescription" title="{{description}}">{{description}}</div>
|
||||
<div class="characterAssetButtons flex-container">
|
||||
<div class="characterAssetDownloadButton right_menu_button fa-fw fa-solid fa-download" title="Download"></div>
|
||||
<div class="characterAssetCheckMark right_menu_button fa-fw fa-solid fa-check" title="Installed"></div>
|
||||
</div>
|
||||
</div>
|
|
@ -3,8 +3,9 @@ TODO:
|
|||
*/
|
||||
//const DEBUG_TONY_SAMA_FORK_MODE = true
|
||||
|
||||
import { getRequestHeaders, callPopup, processDroppedFiles } from '../../../script.js';
|
||||
import { getRequestHeaders, callPopup, processDroppedFiles, eventSource, event_types } from '../../../script.js';
|
||||
import { deleteExtension, extensionNames, getContext, installExtension, renderExtensionTemplateAsync } from '../../extensions.js';
|
||||
import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
|
||||
import { executeSlashCommands } from '../../slash-commands.js';
|
||||
import { getStringHash, isValidUrl } from '../../utils.js';
|
||||
export { MODULE_NAME };
|
||||
|
@ -108,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' });
|
||||
|
@ -199,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>`);
|
||||
}
|
||||
|
||||
|
@ -328,6 +332,41 @@ async function deleteAsset(assetType, filename) {
|
|||
}
|
||||
}
|
||||
|
||||
async function openCharacterBrowser(forceDefault) {
|
||||
const url = forceDefault ? ASSETS_JSON_URL : String($('#assets-json-url-field').val());
|
||||
const fetchResult = await fetch(url, { cache: 'no-cache' });
|
||||
const json = await fetchResult.json();
|
||||
const characters = json.filter(x => x.type === 'character');
|
||||
|
||||
if (!characters.length) {
|
||||
toastr.error('No characters found in the assets list', 'Character browser');
|
||||
return;
|
||||
}
|
||||
|
||||
const template = $(await renderExtensionTemplateAsync(MODULE_NAME, 'market', {}));
|
||||
|
||||
for (const character of characters.sort((a, b) => a.name.localeCompare(b.name))) {
|
||||
const listElement = template.find(character.highlight ? '.contestWinnersList' : '.featuredCharactersList');
|
||||
const characterElement = $(await renderExtensionTemplateAsync(MODULE_NAME, 'character', character));
|
||||
const downloadButton = characterElement.find('.characterAssetDownloadButton');
|
||||
const checkMark = characterElement.find('.characterAssetCheckMark');
|
||||
const isInstalled = isAssetInstalled('character', character.id);
|
||||
|
||||
downloadButton.toggle(!isInstalled).on('click', async () => {
|
||||
downloadButton.toggleClass('fa-download fa-spinner fa-spin');
|
||||
await installAsset(character.url, 'character', character.id);
|
||||
downloadButton.hide();
|
||||
checkMark.show();
|
||||
});
|
||||
|
||||
checkMark.toggle(isInstalled);
|
||||
|
||||
listElement.append(characterElement);
|
||||
}
|
||||
|
||||
callGenericPopup(template, POPUP_TYPE.TEXT, '', { okButton: 'Close', wide: true, large: true, allowVerticalScrolling: true, allowHorizontalScrolling: false });
|
||||
}
|
||||
|
||||
//#############################//
|
||||
// API Calls //
|
||||
//#############################//
|
||||
|
@ -361,6 +400,11 @@ jQuery(async () => {
|
|||
const assetsJsonUrl = windowHtml.find('#assets-json-url-field');
|
||||
assetsJsonUrl.val(ASSETS_JSON_URL);
|
||||
|
||||
const charactersButton = windowHtml.find('#assets-characters-button');
|
||||
charactersButton.on('click', async function () {
|
||||
openCharacterBrowser(false);
|
||||
});
|
||||
|
||||
const connectButton = windowHtml.find('#assets-connect-button');
|
||||
connectButton.on('click', async function () {
|
||||
const url = String(assetsJsonUrl.val());
|
||||
|
@ -397,4 +441,8 @@ jQuery(async () => {
|
|||
|
||||
windowHtml.find('#assets_filters').hide();
|
||||
$('#extensions_settings').append(windowHtml);
|
||||
|
||||
eventSource.on(event_types.OPEN_CHARACTER_LIBRARY, async (forceDefault) => {
|
||||
openCharacterBrowser(forceDefault);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
<div class="flex-container flexFlowColumn padding5">
|
||||
<div class="contestWinners flex-container flexFlowColumn">
|
||||
<h3 class="flex-container alignItemsBaseline justifyCenter" title="These characters are the winners of character design contests and have outstandable quality.">
|
||||
<span data-i18n="Contest Winners">Contest Winners</span>
|
||||
<i class="fa-solid fa-star"></i>
|
||||
</h3>
|
||||
<div class="contestWinnersList characterAssetList">
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="featuredCharacters flex-container flexFlowColumn">
|
||||
<h3 class="flex-container alignItemsBaseline justifyCenter" title="These characters are the finalists of character design contests and have remarkable quality.">
|
||||
<span data-i18n="Featured Characters">Featured Characters</span>
|
||||
<i class="fa-solid fa-thumbs-up"></i>
|
||||
</h3>
|
||||
<div class="featuredCharactersList characterAssetList">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -105,3 +105,54 @@
|
|||
transform: rotate(1turn);
|
||||
}
|
||||
}
|
||||
|
||||
.characterAssetList {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.characterAsset {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
gap: 10px;
|
||||
border: 1px solid var(--SmartThemeBorderColor);
|
||||
background-color: var(--black30a);
|
||||
border-radius: 10px;
|
||||
width: 17%;
|
||||
min-width: 150px;
|
||||
margin: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.characterAssetName {
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.characterAssetImage {
|
||||
max-height: 140px;
|
||||
object-fit: scale-down;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.characterAssetDescription {
|
||||
font-size: 0.75em;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 4;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.characterAssetButtons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
}
|
||||
|
|
|
@ -14,6 +14,10 @@
|
|||
<select id="assets_type_select" class="text_pole flex1">
|
||||
</select>
|
||||
<input id="assets_search" class="text_pole flex1" placeholder="Search" type="search">
|
||||
<div id="assets-characters-button" class="menu_button menu_button_icon">
|
||||
<i class="fa-solid fa-image-portrait"></i>
|
||||
Characters
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-drawer-content" id="assets_menu">
|
||||
</div>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
Enter a URL or the ID of a Fandom wiki page to scrape:
|
||||
</label>
|
||||
<small>
|
||||
<span data-i18n=Examples:">Examples:</span>
|
||||
<span data-i18n="Examples:">Examples:</span>
|
||||
<code>https://harrypotter.fandom.com/</code>
|
||||
<span data-i18n="or">or</span>
|
||||
<code>harrypotter</code>
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
import { renderExtensionTemplateAsync } from '../../extensions.js';
|
||||
import { registerSlashCommand } from '../../slash-commands.js';
|
||||
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
||||
|
||||
jQuery(async () => {
|
||||
const buttons = await renderExtensionTemplateAsync('attachments', 'buttons', {});
|
||||
$('#extensionsMenu').prepend(buttons);
|
||||
|
||||
registerSlashCommand('db', () => document.getElementById('manageAttachments')?.click(), ['databank', 'data-bank'], '– open the data bank', true, true);
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'db',
|
||||
callback: () => document.getElementById('manageAttachments')?.click(),
|
||||
aliases: ['databank', 'data-bank'],
|
||||
helpString: 'Open the data bank',
|
||||
}));
|
||||
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
Don't include the page name!
|
||||
</i>
|
||||
<small>
|
||||
<span data-i18n=Examples:">Examples:</span>
|
||||
<span data-i18n="Examples:">Examples:</span>
|
||||
<code>https://streetcat.wiki/index.php</code>
|
||||
<span data-i18n="or">or</span>
|
||||
<code>https://tcrf.net</code>
|
||||
|
|
|
@ -5,7 +5,9 @@ import { getMessageTimeStamp } from '../../RossAscends-mods.js';
|
|||
import { SECRET_KEYS, secret_state } from '../../secrets.js';
|
||||
import { getMultimodalCaption } from '../shared.js';
|
||||
import { textgen_types, textgenerationwebui_settings } from '../../textgen-settings.js';
|
||||
import { registerSlashCommand } from '../../slash-commands.js';
|
||||
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
||||
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
|
||||
export { MODULE_NAME };
|
||||
|
||||
const MODULE_NAME = 'caption';
|
||||
|
@ -254,6 +256,19 @@ async function onSelectImage(e, prompt, quiet) {
|
|||
return '';
|
||||
}
|
||||
|
||||
const caption = await getCaptionForFile(file, prompt, quiet);
|
||||
form && form.reset();
|
||||
return caption;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a caption for an image file.
|
||||
* @param {File} file Input file
|
||||
* @param {string} prompt Caption prompt
|
||||
* @param {boolean} quiet Suppresses sending a message
|
||||
* @returns {Promise<string>} Generated caption
|
||||
*/
|
||||
async function getCaptionForFile(file, prompt, quiet) {
|
||||
try {
|
||||
setSpinnerIcon();
|
||||
const context = getContext();
|
||||
|
@ -273,7 +288,6 @@ async function onSelectImage(e, prompt, quiet) {
|
|||
return '';
|
||||
}
|
||||
finally {
|
||||
form && form.reset();
|
||||
setImageIcon();
|
||||
}
|
||||
}
|
||||
|
@ -288,9 +302,26 @@ function onRefineModeInput() {
|
|||
* @param {object} args Named parameters
|
||||
* @param {string} prompt Caption prompt
|
||||
*/
|
||||
function captionCommandCallback(args, prompt) {
|
||||
async function captionCommandCallback(args, prompt) {
|
||||
const quiet = isTrueBoolean(args?.quiet);
|
||||
const id = args?.id;
|
||||
|
||||
if (!isNaN(Number(id))) {
|
||||
const message = getContext().chat[id];
|
||||
if (message?.extra?.image) {
|
||||
try {
|
||||
const fetchResult = await fetch(message.extra.image);
|
||||
const blob = await fetchResult.blob();
|
||||
const file = new File([blob], 'image.jpg', { type: blob.type });
|
||||
return await getCaptionForFile(file, prompt, quiet);
|
||||
} catch (error) {
|
||||
toastr.error('Failed to get image from the message. Make sure the image is accessible.');
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
const quiet = isTrueBoolean(args?.quiet);
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'image/*';
|
||||
|
@ -404,12 +435,17 @@ 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>
|
||||
<option data-type="google" value="gemini-pro-vision">gemini-pro-vision</option>
|
||||
<option data-type="google" value="gemini-1.5-flash-latest">gemini-1.5-flash-latest</option>
|
||||
<option data-type="openrouter" value="openai/gpt-4-vision-preview">openai/gpt-4-vision-preview</option>
|
||||
<option data-type="openrouter" value="openai/gpt-4o">openai/gpt-4o</option>
|
||||
<option data-type="openrouter" value="openai/gpt-4-turbo">openai/gpt-4-turbo</option>
|
||||
<option data-type="openrouter" value="haotian-liu/llava-13b">haotian-liu/llava-13b</option>
|
||||
<option data-type="openrouter" value="fireworks/firellava-13b">fireworks/firellava-13b</option>
|
||||
<option data-type="openrouter" value="anthropic/claude-3-haiku">anthropic/claude-3-haiku</option>
|
||||
<option data-type="openrouter" value="anthropic/claude-3-sonnet">anthropic/claude-3-sonnet</option>
|
||||
<option data-type="openrouter" value="anthropic/claude-3-opus">anthropic/claude-3-opus</option>
|
||||
|
@ -418,6 +454,8 @@ jQuery(function () {
|
|||
<option data-type="openrouter" value="anthropic/claude-3-opus:beta">anthropic/claude-3-opus:beta</option>
|
||||
<option data-type="openrouter" value="nousresearch/nous-hermes-2-vision-7b">nousresearch/nous-hermes-2-vision-7b</option>
|
||||
<option data-type="openrouter" value="google/gemini-pro-vision">google/gemini-pro-vision</option>
|
||||
<option data-type="openrouter" value="google/gemini-flash-1.5">google/gemini-flash-1.5</option>
|
||||
<option data-type="openrouter" value="liuhaotian/llava-yi-34b">liuhaotian/llava-yi-34b</option>
|
||||
<option data-type="ollama" value="ollama_current">[Currently selected]</option>
|
||||
<option data-type="ollama" value="bakllava:latest">bakllava:latest</option>
|
||||
<option data-type="ollama" value="llava:latest">llava:latest</option>
|
||||
|
@ -492,5 +530,35 @@ jQuery(function () {
|
|||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
registerSlashCommand('caption', captionCommandCallback, [], '<span class="monospace">quiet=true/false [prompt]</span> - caption an image with an optional prompt and passes the caption down the pipe. Only multimodal sources support custom prompts. Set the "quiet" argument to true to suppress sending a captioned message, default: false.', true, true);
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'caption',
|
||||
callback: captionCommandCallback,
|
||||
returns: 'caption',
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument(
|
||||
'quiet', 'suppress sending a captioned message', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false', ['true', 'false'],
|
||||
),
|
||||
new SlashCommandNamedArgument(
|
||||
'id', 'get image from a message with this ID', [ARGUMENT_TYPE.NUMBER], false, false,
|
||||
),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'prompt', [ARGUMENT_TYPE.STRING], false,
|
||||
),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Caption an image with an optional prompt and passes the caption down the pipe.
|
||||
</div>
|
||||
<div>
|
||||
Only multimodal sources support custom prompts.
|
||||
</div>
|
||||
<div>
|
||||
Provide a message ID to get an image from a message instead of uploading one.
|
||||
</div>
|
||||
<div>
|
||||
Set the "quiet" argument to true to suppress sending a captioned message, default: false.
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
});
|
||||
|
|
|
@ -2,11 +2,13 @@ import { callPopup, eventSource, event_types, generateQuietPrompt, getRequestHea
|
|||
import { dragElement, isMobile } from '../../RossAscends-mods.js';
|
||||
import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js';
|
||||
import { loadMovingUIState, power_user } from '../../power-user.js';
|
||||
import { registerSlashCommand } from '../../slash-commands.js';
|
||||
import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence } from '../../utils.js';
|
||||
import { hideMutedSprites } from '../../group-chats.js';
|
||||
import { isJsonSchemaSupported } from '../../textgen-settings.js';
|
||||
import { debounce_timeout } from '../../constants.js';
|
||||
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
||||
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument } from '../../slash-commands/SlashCommandArgument.js';
|
||||
export { MODULE_NAME };
|
||||
|
||||
const MODULE_NAME = 'expressions';
|
||||
|
@ -906,8 +908,10 @@ async function setSpriteSetCommand(_, folder) {
|
|||
|
||||
$('#expression_override').val(folder.trim());
|
||||
onClickExpressionOverrideButton();
|
||||
removeExpression();
|
||||
moduleWorker();
|
||||
// removeExpression();
|
||||
// moduleWorker();
|
||||
const vnMode = isVisualNovelMode();
|
||||
await sendExpressionCall(folder, lastExpression, true, vnMode);
|
||||
}
|
||||
|
||||
async function classifyCommand(_, text) {
|
||||
|
@ -1967,9 +1971,61 @@ function migrateSettings() {
|
|||
});
|
||||
eventSource.on(event_types.MOVABLE_PANELS_RESET, updateVisualNovelModeDebounced);
|
||||
eventSource.on(event_types.GROUP_UPDATED, updateVisualNovelModeDebounced);
|
||||
registerSlashCommand('sprite', setSpriteSlashCommand, ['emote'], '<span class="monospace">(spriteId)</span> – force sets the sprite for the current character', true, true);
|
||||
registerSlashCommand('spriteoverride', setSpriteSetCommand, ['costume'], '<span class="monospace">(optional folder)</span> – sets an override sprite folder for the current character. If the name starts with a slash or a backslash, selects a sub-folder in the character-named folder. Empty value to reset to default.', true, true);
|
||||
registerSlashCommand('lastsprite', (_, value) => lastExpression[value.trim()] ?? '', [], '<span class="monospace">(charName)</span> – Returns the last set sprite / expression for the named character.', true, true);
|
||||
registerSlashCommand('th', toggleTalkingHeadCommand, ['talkinghead'], '– Character Expressions: toggles <i>Image Type - talkinghead (extras)</i> on/off.', true, true);
|
||||
registerSlashCommand('classify', classifyCommand, [], '<span class="monospace">(text)</span> – performs an emotion classification of the given text and returns a label.', true, true);
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'sprite',
|
||||
aliases: ['emote'],
|
||||
callback: setSpriteSlashCommand,
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'spriteId', [ARGUMENT_TYPE.STRING], true,
|
||||
),
|
||||
],
|
||||
helpString: 'Force sets the sprite for the current character.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'spriteoverride',
|
||||
aliases: ['costume'],
|
||||
callback: setSpriteSetCommand,
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'optional folder', [ARGUMENT_TYPE.STRING], false,
|
||||
),
|
||||
],
|
||||
helpString: 'Sets an override sprite folder for the current character. If the name starts with a slash or a backslash, selects a sub-folder in the character-named folder. Empty value to reset to default.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'lastsprite',
|
||||
callback: (_, value) => lastExpression[value.trim()] ?? '',
|
||||
returns: 'sprite',
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'charName', [ARGUMENT_TYPE.STRING], true,
|
||||
),
|
||||
],
|
||||
helpString: 'Returns the last set sprite / expression for the named character.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'th',
|
||||
callback: toggleTalkingHeadCommand,
|
||||
aliases: ['talkinghead'],
|
||||
helpString: 'Character Expressions: toggles <i>Image Type - talkinghead (extras)</i> on/off.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'classify',
|
||||
callback: classifyCommand,
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'text', [ARGUMENT_TYPE.STRING], true,
|
||||
),
|
||||
],
|
||||
returns: 'emotion classification label for the given text',
|
||||
helpString: `
|
||||
<div>
|
||||
Performs an emotion classification of the given text and returns a label.
|
||||
</div>
|
||||
<div>
|
||||
<strong>Example:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code>/classify I am so happy today!</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
})();
|
||||
|
|
|
@ -8,7 +8,9 @@ import { groups, selected_group } from '../../group-chats.js';
|
|||
import { loadFileToDocument, delay } from '../../utils.js';
|
||||
import { loadMovingUIState } from '../../power-user.js';
|
||||
import { dragElement } from '../../RossAscends-mods.js';
|
||||
import { registerSlashCommand } from '../../slash-commands.js';
|
||||
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
||||
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
|
||||
|
||||
const extensionName = 'gallery';
|
||||
const extensionFolderPath = `scripts/extensions/${extensionName}/`;
|
||||
|
@ -415,8 +417,26 @@ function viewWithDragbox(items) {
|
|||
|
||||
|
||||
// Registers a simple command for opening the char gallery.
|
||||
registerSlashCommand('show-gallery', showGalleryCommand, ['sg'], '– shows the gallery', true, true);
|
||||
registerSlashCommand('list-gallery', listGalleryCommand, ['lg'], '<span class="monospace">[optional char=charName] [optional group=groupName]</span> – list images in the gallery of the current char / group or a specified char / group', true, true);
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'show-gallery',
|
||||
aliases: ['sg'],
|
||||
callback: showGalleryCommand,
|
||||
helpString: 'Shows the gallery.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'list-gallery',
|
||||
aliases: ['lg'],
|
||||
callback: listGalleryCommand,
|
||||
returns: 'list of images',
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument(
|
||||
'char', 'character name', [ARGUMENT_TYPE.STRING], false,
|
||||
),
|
||||
new SlashCommandNamedArgument(
|
||||
'group', 'group name', [ARGUMENT_TYPE.STRING], false,
|
||||
),
|
||||
],
|
||||
helpString: 'List images in the gallery of the current char / group or a specified char / group.',
|
||||
}));
|
||||
|
||||
|
||||
function showGalleryCommand(args) {
|
||||
showCharGallery();
|
||||
|
|
|
@ -16,11 +16,14 @@ import {
|
|||
getMaxContextSize,
|
||||
} from '../../../script.js';
|
||||
import { is_group_generating, selected_group } from '../../group-chats.js';
|
||||
import { registerSlashCommand } from '../../slash-commands.js';
|
||||
import { loadMovingUIState } from '../../power-user.js';
|
||||
import { dragElement } from '../../RossAscends-mods.js';
|
||||
import { getTextTokens, getTokenCountAsync, tokenizers } from '../../tokenizers.js';
|
||||
import { debounce_timeout } from '../../constants.js';
|
||||
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
||||
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
|
||||
import { resolveVariable } from '../../variables.js';
|
||||
export { MODULE_NAME };
|
||||
|
||||
const MODULE_NAME = '1_memory';
|
||||
|
@ -416,7 +419,7 @@ async function forceSummarizeChat() {
|
|||
console.log(`Skipping WIAN? ${skipWIAN}`);
|
||||
if (!context.chatId) {
|
||||
toastr.warning('No chat selected');
|
||||
return;
|
||||
return '';
|
||||
}
|
||||
|
||||
toastr.info('Summarizing chat...', 'Please wait');
|
||||
|
@ -424,7 +427,42 @@ async function forceSummarizeChat() {
|
|||
|
||||
if (!value) {
|
||||
toastr.warning('Failed to summarize chat');
|
||||
return;
|
||||
return '';
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for the summarize command.
|
||||
* @param {object} args Command arguments
|
||||
* @param {string} text Text to summarize
|
||||
*/
|
||||
async function summarizeCallback(args, text) {
|
||||
text = text.trim();
|
||||
|
||||
// Using forceSummarizeChat to summarize the current chat
|
||||
if (!text) {
|
||||
return await forceSummarizeChat();
|
||||
}
|
||||
|
||||
const source = args.source || extension_settings.memory.source;
|
||||
const prompt = substituteParams((resolveVariable(args.prompt) || extension_settings.memory.prompt)?.replace(/{{words}}/gi, extension_settings.memory.promptWords));
|
||||
|
||||
try {
|
||||
switch (source) {
|
||||
case summary_sources.extras:
|
||||
return await callExtrasSummarizeAPI(text);
|
||||
case summary_sources.main:
|
||||
return await generateRaw(text, '', false, false, prompt, extension_settings.memory.overrideResponseLength);
|
||||
default:
|
||||
toastr.warning('Invalid summarization source specified');
|
||||
return '';
|
||||
}
|
||||
} catch (error) {
|
||||
toastr.error(String(error), 'Failed to summarize text');
|
||||
console.log(error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -668,37 +706,18 @@ async function summarizeChatExtras(context) {
|
|||
// perform the summarization API call
|
||||
try {
|
||||
inApiCall = true;
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/summarize';
|
||||
const summary = await callExtrasSummarizeAPI(resultingString);
|
||||
const newContext = getContext();
|
||||
|
||||
const apiResult = await doExtrasFetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Bypass-Tunnel-Reminder': 'bypass',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
text: resultingString,
|
||||
params: {},
|
||||
}),
|
||||
});
|
||||
|
||||
if (apiResult.ok) {
|
||||
const data = await apiResult.json();
|
||||
const summary = data.summary;
|
||||
|
||||
const newContext = getContext();
|
||||
|
||||
// something changed during summarization request
|
||||
if (newContext.groupId !== context.groupId
|
||||
|| newContext.chatId !== context.chatId
|
||||
|| (!newContext.groupId && (newContext.characterId !== context.characterId))) {
|
||||
console.log('Context changed, summary discarded');
|
||||
return;
|
||||
}
|
||||
|
||||
setMemoryContext(summary, true);
|
||||
// something changed during summarization request
|
||||
if (newContext.groupId !== context.groupId
|
||||
|| newContext.chatId !== context.chatId
|
||||
|| (!newContext.groupId && (newContext.characterId !== context.characterId))) {
|
||||
console.log('Context changed, summary discarded');
|
||||
return;
|
||||
}
|
||||
|
||||
setMemoryContext(summary, true);
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error);
|
||||
|
@ -708,6 +727,40 @@ async function summarizeChatExtras(context) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the Extras API to summarize the provided text.
|
||||
* @param {string} text Text to summarize
|
||||
* @returns {Promise<string>} Summarized text
|
||||
*/
|
||||
async function callExtrasSummarizeAPI(text) {
|
||||
if (!modules.includes('summarize')) {
|
||||
throw new Error('Summarize module is not enabled in Extras API');
|
||||
}
|
||||
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/summarize';
|
||||
|
||||
const apiResult = await doExtrasFetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Bypass-Tunnel-Reminder': 'bypass',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
text: text,
|
||||
params: {},
|
||||
}),
|
||||
});
|
||||
|
||||
if (apiResult.ok) {
|
||||
const data = await apiResult.json();
|
||||
const summary = data.summary;
|
||||
return summary;
|
||||
}
|
||||
|
||||
throw new Error('Extras API call failed');
|
||||
}
|
||||
|
||||
function onMemoryRestoreClick() {
|
||||
const context = getContext();
|
||||
const content = $('#memory_contents').val();
|
||||
|
@ -751,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;
|
||||
|
@ -865,5 +915,16 @@ jQuery(async function () {
|
|||
eventSource.on(event_types.MESSAGE_EDITED, onChatEvent);
|
||||
eventSource.on(event_types.MESSAGE_SWIPED, onChatEvent);
|
||||
eventSource.on(event_types.CHAT_CHANGED, onChatEvent);
|
||||
registerSlashCommand('summarize', forceSummarizeChat, [], '– forces the summarization of the current chat using the Main API', true, true);
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'summarize',
|
||||
callback: summarizeCallback,
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument('source', 'API to use for summarization', [ARGUMENT_TYPE.STRING], false, false, '', ['main', 'extras']),
|
||||
new SlashCommandNamedArgument('prompt', 'prompt to use for summarization', [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.VARIABLE_NAME], false, false, ''),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument('text to summarize', [ARGUMENT_TYPE.STRING], false, false, ''),
|
||||
],
|
||||
helpString: 'Summarizes the given text. If no text is provided, the current chat will be summarized. Can specify the source and the prompt to use.',
|
||||
}));
|
||||
});
|
||||
|
|
|
@ -30,7 +30,10 @@
|
|||
<span>Ctrl+Enter to execute</span>
|
||||
</label>
|
||||
</div>
|
||||
<textarea class="monospace" id="qr--modal-message"></textarea>
|
||||
<div id="qr--modal-messageHolder">
|
||||
<pre id="qr--modal-messageSyntax"><code id="qr--modal-messageSyntaxInner" class="hljs language-stscript"></code></pre>
|
||||
<textarea class="monospace" id="qr--modal-message" spellcheck="false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -94,14 +97,27 @@
|
|||
|
||||
|
||||
<h3>Testing</h3>
|
||||
<div id="qr--modal-execute" class="menu_button" title="Execute the quick reply now">
|
||||
<i class="fa-solid fa-play"></i>
|
||||
Execute
|
||||
<div id="qr--modal-executeButtons">
|
||||
<div id="qr--modal-execute" class="qr--modal-executeButton menu_button" title="Execute the quick reply now">
|
||||
<i class="fa-solid fa-play"></i>
|
||||
Execute
|
||||
</div>
|
||||
<div id="qr--modal-pause" class="qr--modal-executeButton menu_button" title="Pause / continue execution">
|
||||
<span class="qr--modal-executeComboIcon">
|
||||
<i class="fa-solid fa-play"></i>
|
||||
<i class="fa-solid fa-pause"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div id="qr--modal-stop" class="qr--modal-executeButton menu_button" title="Abort execution">
|
||||
<i class="fa-solid fa-stop"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div id="qr--modal-executeProgress"></div>
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="qr--modal-executeHide">
|
||||
<span> Hide editor while executing</span>
|
||||
</label>
|
||||
<div id="qr--modal-executeErrors"></div>
|
||||
<div id="qr--modal-executeResult"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -183,14 +183,16 @@ const init = async () => {
|
|||
;
|
||||
if (!qr) {
|
||||
let [setName, ...qrName] = name.split('.');
|
||||
name = qrName.join('.');
|
||||
qrName = qrName.join('.');
|
||||
let qrs = QuickReplySet.get(setName);
|
||||
if (qrs) {
|
||||
qr = qrs.qrList.find(it=>it.label == name);
|
||||
qr = qrs.qrList.find(it=>it.label == qrName);
|
||||
}
|
||||
}
|
||||
if (qr && qr.onExecute) {
|
||||
return await qr.execute(args);
|
||||
return await qr.execute(args, false, true);
|
||||
} else {
|
||||
throw new Error(`No Quick Reply found for "${name}".`);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import { POPUP_TYPE, Popup } from '../../../popup.js';
|
||||
import { getSortableDelay } from '../../../utils.js';
|
||||
import { setSlashCommandAutoComplete } from '../../../slash-commands.js';
|
||||
import { SlashCommandAbortController } from '../../../slash-commands/SlashCommandAbortController.js';
|
||||
import { SlashCommandParserError } from '../../../slash-commands/SlashCommandParserError.js';
|
||||
import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
|
||||
import { debounce, getSortableDelay } from '../../../utils.js';
|
||||
import { log, warn } from '../index.js';
|
||||
import { QuickReplyContextLink } from './QuickReplyContextLink.js';
|
||||
import { QuickReplySet } from './QuickReplySet.js';
|
||||
|
@ -47,9 +51,14 @@ export class QuickReply {
|
|||
/**@type {Popup}*/ editorPopup;
|
||||
|
||||
/**@type {HTMLElement}*/ editorExecuteBtn;
|
||||
/**@type {HTMLElement}*/ editorExecuteBtnPause;
|
||||
/**@type {HTMLElement}*/ editorExecuteBtnStop;
|
||||
/**@type {HTMLElement}*/ editorExecuteProgress;
|
||||
/**@type {HTMLElement}*/ editorExecuteErrors;
|
||||
/**@type {HTMLElement}*/ editorExecuteResult;
|
||||
/**@type {HTMLInputElement}*/ editorExecuteHide;
|
||||
/**@type {Promise}*/ editorExecutePromise;
|
||||
/**@type {SlashCommandAbortController}*/ abortController;
|
||||
|
||||
|
||||
get hasContext() {
|
||||
|
@ -225,15 +234,43 @@ export class QuickReply {
|
|||
const updateWrap = () => {
|
||||
if (wrap.checked) {
|
||||
message.style.whiteSpace = 'pre-wrap';
|
||||
messageSyntaxInner.style.whiteSpace = 'pre-wrap';
|
||||
} else {
|
||||
message.style.whiteSpace = 'pre';
|
||||
messageSyntaxInner.style.whiteSpace = 'pre';
|
||||
}
|
||||
updateScrollDebounced();
|
||||
};
|
||||
const updateScroll = (evt) => {
|
||||
let left = message.scrollLeft;
|
||||
let top = message.scrollTop;
|
||||
if (evt) {
|
||||
evt.preventDefault();
|
||||
left = message.scrollLeft + evt.deltaX;
|
||||
top = message.scrollTop + evt.deltaY;
|
||||
message.scrollTo({
|
||||
behavior: 'instant',
|
||||
left,
|
||||
top,
|
||||
});
|
||||
}
|
||||
messageSyntaxInner.scrollTo({
|
||||
behavior: 'instant',
|
||||
left,
|
||||
top,
|
||||
});
|
||||
};
|
||||
const updateScrollDebounced = updateScroll;
|
||||
const updateSyntax = ()=>{
|
||||
messageSyntaxInner.innerHTML = hljs.highlight(`${message.value}${message.value.slice(-1) == '\n' ? ' ' : ''}`, { language:'stscript', ignoreIllegals:true })?.value;
|
||||
};
|
||||
/**@type {HTMLInputElement}*/
|
||||
const tabSize = dom.querySelector('#qr--modal-tabSize');
|
||||
tabSize.value = JSON.parse(localStorage.getItem('qr--tabSize') ?? '4');
|
||||
const updateTabSize = () => {
|
||||
message.style.tabSize = tabSize.value;
|
||||
messageSyntaxInner.style.tabSize = tabSize.value;
|
||||
updateScrollDebounced();
|
||||
};
|
||||
tabSize.addEventListener('change', () => {
|
||||
localStorage.setItem('qr--tabSize', JSON.stringify(Number(tabSize.value)));
|
||||
|
@ -247,14 +284,15 @@ export class QuickReply {
|
|||
});
|
||||
/**@type {HTMLTextAreaElement}*/
|
||||
const message = dom.querySelector('#qr--modal-message');
|
||||
updateWrap();
|
||||
updateTabSize();
|
||||
message.value = this.message;
|
||||
message.addEventListener('input', () => {
|
||||
updateSyntax();
|
||||
this.updateMessage(message.value);
|
||||
updateScrollDebounced();
|
||||
});
|
||||
setSlashCommandAutoComplete(message, true);
|
||||
//TODO move tab support for textarea into its own helper(?) and use for both this and .editor_maximize
|
||||
message.addEventListener('keydown', (evt) => {
|
||||
message.addEventListener('keydown', async(evt) => {
|
||||
if (evt.key == 'Tab' && !evt.shiftKey && !evt.ctrlKey && !evt.altKey) {
|
||||
evt.preventDefault();
|
||||
const start = message.selectionStart;
|
||||
|
@ -265,12 +303,12 @@ export class QuickReply {
|
|||
message.value = `${message.value.substring(0, lineStart)}${message.value.substring(lineStart, end).replace(/\n/g, '\n\t')}${message.value.substring(end)}`;
|
||||
message.selectionStart = start + 1;
|
||||
message.selectionEnd = end + count;
|
||||
this.updateMessage(message.value);
|
||||
updateSyntax();
|
||||
} else {
|
||||
message.value = `${message.value.substring(0, start)}\t${message.value.substring(end)}`;
|
||||
message.selectionStart = start + 1;
|
||||
message.selectionEnd = end + 1;
|
||||
this.updateMessage(message.value);
|
||||
updateSyntax();
|
||||
}
|
||||
} else if (evt.key == 'Tab' && evt.shiftKey && !evt.ctrlKey && !evt.altKey) {
|
||||
evt.preventDefault();
|
||||
|
@ -281,15 +319,47 @@ export class QuickReply {
|
|||
message.value = `${message.value.substring(0, lineStart)}${message.value.substring(lineStart, end).replace(/\n\t/g, '\n')}${message.value.substring(end)}`;
|
||||
message.selectionStart = start - 1;
|
||||
message.selectionEnd = end - count;
|
||||
this.updateMessage(message.value);
|
||||
updateSyntax();
|
||||
} else if (evt.key == 'Enter' && evt.ctrlKey && !evt.shiftKey && !evt.altKey) {
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
if (executeShortcut.checked) {
|
||||
this.executeFromEditor();
|
||||
const selectionStart = message.selectionStart;
|
||||
const selectionEnd = message.selectionEnd;
|
||||
message.blur();
|
||||
await this.executeFromEditor();
|
||||
if (document.activeElement != message) {
|
||||
message.focus();
|
||||
message.selectionStart = selectionStart;
|
||||
message.selectionEnd = selectionEnd;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
message.addEventListener('wheel', (evt)=>{
|
||||
updateScrollDebounced(evt);
|
||||
});
|
||||
message.addEventListener('scroll', (evt)=>{
|
||||
updateScrollDebounced();
|
||||
});
|
||||
/** @type {any} */
|
||||
const resizeListener = debounce((evt) => {
|
||||
updateSyntax();
|
||||
updateScrollDebounced(evt);
|
||||
if (document.activeElement == message) {
|
||||
message.blur();
|
||||
message.focus();
|
||||
}
|
||||
});
|
||||
window.addEventListener('resize', resizeListener);
|
||||
message.style.color = 'transparent';
|
||||
message.style.background = 'transparent';
|
||||
message.style.setProperty('text-shadow', 'none', 'important');
|
||||
/**@type {HTMLElement}*/
|
||||
const messageSyntaxInner = dom.querySelector('#qr--modal-messageSyntaxInner');
|
||||
updateSyntax();
|
||||
updateWrap();
|
||||
updateTabSize();
|
||||
|
||||
// context menu
|
||||
/**@type {HTMLTemplateElement}*/
|
||||
|
@ -414,9 +484,15 @@ export class QuickReply {
|
|||
this.updateContext();
|
||||
});
|
||||
|
||||
/**@type {HTMLElement}*/
|
||||
const executeProgress = dom.querySelector('#qr--modal-executeProgress');
|
||||
this.editorExecuteProgress = executeProgress;
|
||||
/**@type {HTMLElement}*/
|
||||
const executeErrors = dom.querySelector('#qr--modal-executeErrors');
|
||||
this.editorExecuteErrors = executeErrors;
|
||||
/**@type {HTMLElement}*/
|
||||
const executeResult = dom.querySelector('#qr--modal-executeResult');
|
||||
this.editorExecuteResult = executeResult;
|
||||
/**@type {HTMLInputElement}*/
|
||||
const executeHide = dom.querySelector('#qr--modal-executeHide');
|
||||
this.editorExecuteHide = executeHide;
|
||||
|
@ -426,8 +502,30 @@ export class QuickReply {
|
|||
executeBtn.addEventListener('click', async()=>{
|
||||
await this.executeFromEditor();
|
||||
});
|
||||
/**@type {HTMLElement}*/
|
||||
const executeBtnPause = dom.querySelector('#qr--modal-pause');
|
||||
this.editorExecuteBtnPause = executeBtnPause;
|
||||
executeBtnPause.addEventListener('click', async()=>{
|
||||
if (this.abortController) {
|
||||
if (this.abortController.signal.paused) {
|
||||
this.abortController.continue('Continue button clicked');
|
||||
this.editorExecuteProgress.classList.remove('qr--paused');
|
||||
} else {
|
||||
this.abortController.pause('Pause button clicked');
|
||||
this.editorExecuteProgress.classList.add('qr--paused');
|
||||
}
|
||||
}
|
||||
});
|
||||
/**@type {HTMLElement}*/
|
||||
const executeBtnStop = dom.querySelector('#qr--modal-stop');
|
||||
this.editorExecuteBtnStop = executeBtnStop;
|
||||
executeBtnStop.addEventListener('click', async()=>{
|
||||
this.abortController?.abort('Stop button clicked');
|
||||
});
|
||||
|
||||
await popupResult;
|
||||
|
||||
window.removeEventListener('resize', resizeListener);
|
||||
} else {
|
||||
warn('failed to fetch qrEditor template');
|
||||
}
|
||||
|
@ -436,21 +534,54 @@ export class QuickReply {
|
|||
async executeFromEditor() {
|
||||
if (this.editorExecutePromise) return;
|
||||
this.editorExecuteBtn.classList.add('qr--busy');
|
||||
this.editorExecuteProgress.style.setProperty('--prog', '0');
|
||||
this.editorExecuteErrors.classList.remove('qr--hasErrors');
|
||||
this.editorExecuteResult.classList.remove('qr--hasResult');
|
||||
this.editorExecuteProgress.classList.remove('qr--error');
|
||||
this.editorExecuteProgress.classList.remove('qr--success');
|
||||
this.editorExecuteProgress.classList.remove('qr--paused');
|
||||
this.editorExecuteProgress.classList.remove('qr--aborted');
|
||||
this.editorExecuteErrors.innerHTML = '';
|
||||
this.editorExecuteResult.innerHTML = '';
|
||||
if (this.editorExecuteHide.checked) {
|
||||
this.editorPopup.dom.classList.add('qr--hide');
|
||||
}
|
||||
try {
|
||||
this.editorExecutePromise = this.execute();
|
||||
await this.editorExecutePromise;
|
||||
this.editorExecutePromise = this.execute({}, true);
|
||||
const result = await this.editorExecutePromise;
|
||||
if (this.abortController?.signal?.aborted) {
|
||||
this.editorExecuteProgress.classList.add('qr--aborted');
|
||||
} else {
|
||||
this.editorExecuteResult.textContent = result?.toString();
|
||||
this.editorExecuteResult.classList.add('qr--hasResult');
|
||||
this.editorExecuteProgress.classList.add('qr--success');
|
||||
}
|
||||
this.editorExecuteProgress.classList.remove('qr--paused');
|
||||
} catch (ex) {
|
||||
this.editorExecuteErrors.textContent = ex.message;
|
||||
this.editorExecuteErrors.classList.add('qr--hasErrors');
|
||||
this.editorExecuteProgress.classList.add('qr--error');
|
||||
this.editorExecuteProgress.classList.remove('qr--paused');
|
||||
if (ex instanceof SlashCommandParserError) {
|
||||
this.editorExecuteErrors.innerHTML = `
|
||||
<div>${ex.message}</div>
|
||||
<div>Line: ${ex.line} Column: ${ex.column}</div>
|
||||
<pre style="text-align:left;">${ex.hint}</pre>
|
||||
`;
|
||||
} else {
|
||||
this.editorExecuteErrors.innerHTML = `
|
||||
<div>${ex.message}</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
this.editorExecutePromise = null;
|
||||
this.editorExecuteBtn.classList.remove('qr--busy');
|
||||
this.editorPopup.dom.classList.remove('qr--hide');
|
||||
}
|
||||
|
||||
updateEditorProgress(done, total) {
|
||||
this.editorExecuteProgress.style.setProperty('--prog', `${done / total * 100}`);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -526,12 +657,22 @@ export class QuickReply {
|
|||
}
|
||||
|
||||
|
||||
async execute(args = {}) {
|
||||
async execute(args = {}, isEditor = false, isRun = false) {
|
||||
if (this.message?.length > 0 && this.onExecute) {
|
||||
const message = this.message.replace(/\{\{arg::([^}]+)\}\}/g, (_, key) => {
|
||||
return args[key] ?? '';
|
||||
const scope = new SlashCommandScope();
|
||||
for (const key of Object.keys(args)) {
|
||||
scope.setMacro(`arg::${key}`, args[key]);
|
||||
}
|
||||
if (isEditor) {
|
||||
this.abortController = new SlashCommandAbortController();
|
||||
}
|
||||
return await this.onExecute(this, {
|
||||
message:this.message,
|
||||
isAutoExecute: args.isAutoExecute ?? false,
|
||||
isEditor,
|
||||
isRun,
|
||||
scope,
|
||||
});
|
||||
return await this.onExecute(this, message, args.isAutoExecute ?? false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { getRequestHeaders, substituteParams } from '../../../../script.js';
|
||||
import { executeSlashCommands } from '../../../slash-commands.js';
|
||||
import { executeSlashCommands, executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions } from '../../../slash-commands.js';
|
||||
import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
|
||||
import { debounceAsync, warn } from '../index.js';
|
||||
import { QuickReply } from './QuickReply.js';
|
||||
|
||||
|
@ -100,15 +101,29 @@ export class QuickReplySet {
|
|||
|
||||
|
||||
/**
|
||||
* @param {QuickReply} qr
|
||||
* @param {String} [message] - optional altered message to be used
|
||||
*
|
||||
* @param {QuickReply} qr The QR to execute.
|
||||
* @param {object} options
|
||||
* @param {string} [options.message] (null) altered message to be used
|
||||
* @param {boolean} [options.isAutoExecute] (false) whether the execution is triggered by auto execute
|
||||
* @param {boolean} [options.isEditor] (false) whether the execution is triggered by the QR editor
|
||||
* @param {boolean} [options.isRun] (false) whether the execution is triggered by /run or /: (window.executeQuickReplyByName)
|
||||
* @param {SlashCommandScope} [options.scope] (null) scope to be used when running the command
|
||||
* @returns
|
||||
*/
|
||||
async execute(qr, message = null, isAutoExecute = false) {
|
||||
async executeWithOptions(qr, options = {}) {
|
||||
options = Object.assign({
|
||||
message:null,
|
||||
isAutoExecute:false,
|
||||
isEditor:false,
|
||||
isRun:false,
|
||||
scope:null,
|
||||
}, options);
|
||||
/**@type {HTMLTextAreaElement}*/
|
||||
const ta = document.querySelector('#send_textarea');
|
||||
const finalMessage = message ?? qr.message;
|
||||
const finalMessage = options.message ?? qr.message;
|
||||
let input = ta.value;
|
||||
if (!isAutoExecute && this.injectInput && input.length > 0) {
|
||||
if (!options.isAutoExecute && !options.isEditor && !options.isRun && this.injectInput && input.length > 0) {
|
||||
if (this.placeBeforeInput) {
|
||||
input = `${finalMessage} ${input}`;
|
||||
} else {
|
||||
|
@ -119,7 +134,24 @@ export class QuickReplySet {
|
|||
}
|
||||
|
||||
if (input[0] == '/' && !this.disableSend) {
|
||||
const result = await executeSlashCommands(input);
|
||||
let result;
|
||||
if (options.isAutoExecute || options.isRun) {
|
||||
result = await executeSlashCommandsWithOptions(input, {
|
||||
handleParserErrors: true,
|
||||
scope: options.scope,
|
||||
});
|
||||
} else if (options.isEditor) {
|
||||
result = await executeSlashCommandsWithOptions(input, {
|
||||
handleParserErrors: false,
|
||||
scope: options.scope,
|
||||
abortController: qr.abortController,
|
||||
onProgress: (done, total) => qr.updateEditorProgress(done, total),
|
||||
});
|
||||
} else {
|
||||
result = await executeSlashCommandsOnChatInput(input, {
|
||||
scope: options.scope,
|
||||
});
|
||||
}
|
||||
return typeof result === 'object' ? result?.pipe : '';
|
||||
}
|
||||
|
||||
|
@ -131,6 +163,18 @@ export class QuickReplySet {
|
|||
document.querySelector('#send_but').click();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {QuickReply} qr
|
||||
* @param {String} [message] - optional altered message to be used
|
||||
* @param {SlashCommandScope} [scope] - optional scope to be used when running the command
|
||||
*/
|
||||
async execute(qr, message = null, isAutoExecute = false, scope = null) {
|
||||
return this.executeWithOptions(qr, {
|
||||
message,
|
||||
isAutoExecute,
|
||||
scope,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@ -152,7 +196,7 @@ export class QuickReplySet {
|
|||
}
|
||||
|
||||
hookQuickReply(qr) {
|
||||
qr.onExecute = (_, message, isAutoExecute)=>this.execute(qr, message, isAutoExecute);
|
||||
qr.onExecute = (_, options)=>this.executeWithOptions(qr, options);
|
||||
qr.onDelete = ()=>this.removeQuickReply(qr);
|
||||
qr.onUpdate = ()=>this.save();
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { registerSlashCommand } from '../../../slash-commands.js';
|
||||
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 { isTrueBoolean } from '../../../utils.js';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { QuickReplyApi } from '../api/QuickReplyApi.js';
|
||||
|
@ -17,46 +19,331 @@ export class SlashCommandHandler {
|
|||
|
||||
|
||||
init() {
|
||||
registerSlashCommand('qr', (_, value) => this.executeQuickReplyByIndex(Number(value)), [], '<span class="monospace">(number)</span> – activates the specified Quick Reply', true, true);
|
||||
registerSlashCommand('qrset', ()=>toastr.warning('The command /qrset has been deprecated. Use /qr-set, /qr-set-on, and /qr-set-off instead.'), [], '<strong>DEPRECATED</strong> – The command /qrset has been deprecated. Use /qr-set, /qr-set-on, and /qr-set-off instead.', true, true);
|
||||
registerSlashCommand('qr-set', (args, value)=>this.toggleGlobalSet(value, args), [], '<span class="monospace">[visible=true] (number)</span> – toggle global QR set', true, true);
|
||||
registerSlashCommand('qr-set-on', (args, value)=>this.addGlobalSet(value, args), [], '<span class="monospace">[visible=true] (number)</span> – activate global QR set', true, true);
|
||||
registerSlashCommand('qr-set-off', (_, value)=>this.removeGlobalSet(value), [], '<span class="monospace">(number)</span> – deactivate global QR set', true, true);
|
||||
registerSlashCommand('qr-chat-set', (args, value)=>this.toggleChatSet(value, args), [], '<span class="monospace">[visible=true] (number)</span> – toggle chat QR set', true, true);
|
||||
registerSlashCommand('qr-chat-set-on', (args, value)=>this.addChatSet(value, args), [], '<span class="monospace">[visible=true] (number)</span> – activate chat QR set', true, true);
|
||||
registerSlashCommand('qr-chat-set-off', (_, value)=>this.removeChatSet(value), [], '<span class="monospace">(number)</span> – deactivate chat QR set', true, true);
|
||||
registerSlashCommand('qr-set-list', (_, value)=>this.listSets(value ?? 'all'), [], '(all|global|chat) – gets a list of the names of all quick reply sets', true, true);
|
||||
registerSlashCommand('qr-list', (_, value)=>this.listQuickReplies(value), [], '(set name) – gets a list of the names of all quick replies in this quick reply set', true, true);
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr',
|
||||
callback: (_, value) => this.executeQuickReplyByIndex(Number(value)),
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'number', [ARGUMENT_TYPE.NUMBER], true,
|
||||
),
|
||||
],
|
||||
helpString: 'Activates the specified Quick Reply',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qrset',
|
||||
callback: () => toastr.warning('The command /qrset has been deprecated. Use /qr-set, /qr-set-on, and /qr-set-off instead.'),
|
||||
helpString: '<strong>DEPRECATED</strong> – The command /qrset has been deprecated. Use /qr-set, /qr-set-on, and /qr-set-off instead.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set',
|
||||
callback: (args, value) => this.toggleGlobalSet(value, args),
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument(
|
||||
'visible', 'set visibility', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true',
|
||||
),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'QR set name', [ARGUMENT_TYPE.STRING], true,
|
||||
),
|
||||
],
|
||||
helpString: 'Toggle global QR set',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-on',
|
||||
callback: (args, value) => this.addGlobalSet(value, args),
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument(
|
||||
'visible', 'set visibility', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true',
|
||||
),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'QR set name', [ARGUMENT_TYPE.STRING], true,
|
||||
),
|
||||
],
|
||||
helpString: 'Activate global QR set',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-off',
|
||||
callback: (_, value) => this.removeGlobalSet(value),
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'QR set name', [ARGUMENT_TYPE.STRING], true,
|
||||
),
|
||||
],
|
||||
helpString: 'Deactivate global QR set',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-chat-set',
|
||||
callback: (args, value) => this.toggleChatSet(value, args),
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument(
|
||||
'visible', 'set visibility', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true',
|
||||
),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'QR set name', [ARGUMENT_TYPE.STRING], true,
|
||||
),
|
||||
],
|
||||
helpString: 'Toggle chat QR set',
|
||||
}));
|
||||
|
||||
const qrArgs = `
|
||||
label - string - text on the button, e.g., label=MyButton
|
||||
set - string - name of the QR set, e.g., set=PresetName1
|
||||
hidden - bool - whether the button should be hidden, e.g., hidden=true
|
||||
startup - bool - auto execute on app startup, e.g., startup=true
|
||||
user - bool - auto execute on user message, e.g., user=true
|
||||
bot - bool - auto execute on AI message, e.g., bot=true
|
||||
load - bool - auto execute on chat load, e.g., load=true
|
||||
group - bool - auto execute on group member selection, e.g., group=true
|
||||
title - string - title / tooltip to be shown on button, e.g., title="My Fancy Button"
|
||||
`.trim();
|
||||
const qrUpdateArgs = `
|
||||
newlabel - string - new text for the button, e.g. newlabel=MyRenamedButton
|
||||
${qrArgs}
|
||||
`.trim();
|
||||
registerSlashCommand('qr-create', (args, message)=>this.createQuickReply(args, message), [], `<span class="monospace" style="white-space:pre-line;">[arguments] (message)\n arguments:\n ${qrArgs}</span> – creates a new Quick Reply, example: <tt>/qr-create set=MyPreset label=MyButton /echo 123</tt>`, true, true);
|
||||
registerSlashCommand('qr-update', (args, message)=>this.updateQuickReply(args, message), [], `<span class="monospace" style="white-space:pre-line;">[arguments] (message)\n arguments:\n ${qrUpdateArgs}</span> – updates Quick Reply, example: <tt>/qr-update set=MyPreset label=MyButton newlabel=MyRenamedButton /echo 123</tt>`, true, true);
|
||||
registerSlashCommand('qr-delete', (args, name)=>this.deleteQuickReply(args, name), [], '<span class="monospace">set=string [label]</span> – deletes Quick Reply', true, true);
|
||||
registerSlashCommand('qr-contextadd', (args, name)=>this.createContextItem(args, name), [], '<span class="monospace">set=string label=string [chain=false] (preset name)</span> – add context menu preset to a QR, example: <tt>/qr-contextadd set=MyPreset label=MyButton chain=true MyOtherPreset</tt>', true, true);
|
||||
registerSlashCommand('qr-contextdel', (args, name)=>this.deleteContextItem(args, name), [], '<span class="monospace">set=string label=string (preset name)</span> – remove context menu preset from a QR, example: <tt>/qr-contextdel set=MyPreset label=MyButton MyOtherPreset</tt>', true, true);
|
||||
registerSlashCommand('qr-contextclear', (args, label)=>this.clearContextMenu(args, label), [], '<span class="monospace">set=string (label)</span> – remove all context menu presets from a QR, example: <tt>/qr-contextclear set=MyPreset MyButton</tt>', true, true);
|
||||
const presetArgs = `
|
||||
nosend - bool - disable send / insert in user input (invalid for slash commands)
|
||||
before - bool - place QR before user input
|
||||
inject - bool - inject user input automatically (if disabled use {{input}})
|
||||
`.trim();
|
||||
registerSlashCommand('qr-set-create', (args, name)=>this.createSet(name, args), ['qr-presetadd'], `<span class="monospace" style="white-space:pre-line;">[arguments] (name)\n arguments:\n ${presetArgs}</span> – create a new preset (overrides existing ones), example: <tt>/qr-set-add MyNewPreset</tt>`, true, true);
|
||||
registerSlashCommand('qr-set-update', (args, name)=>this.updateSet(name, args), ['qr-presetupdate'], `<span class="monospace" style="white-space:pre-line;">[arguments] (name)\n arguments:\n ${presetArgs}</span> – update an existing preset, example: <tt>/qr-set-update enabled=false MyPreset</tt>`, true, true);
|
||||
registerSlashCommand('qr-set-delete', (args, name)=>this.deleteSet(name), ['qr-presetdelete'], `<span class="monospace" style="white-space:pre-line;">(name)\n arguments:\n ${presetArgs}</span> – delete an existing preset, example: <tt>/qr-set-delete MyPreset</tt>`, true, true);
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-chat-set-on',
|
||||
callback: (args, value) => this.addChatSet(value, args),
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument(
|
||||
'visible', 'whether the QR set should be visible', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true', ['true', 'false'],
|
||||
),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'QR set name', [ARGUMENT_TYPE.STRING], true,
|
||||
),
|
||||
],
|
||||
helpString: 'Activate chat QR set',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-chat-set-off',
|
||||
callback: (_, value) => this.removeChatSet(value),
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'QR set name', [ARGUMENT_TYPE.STRING], true,
|
||||
),
|
||||
],
|
||||
helpString: 'Deactivate chat QR set',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-list',
|
||||
callback: (_, value) => this.listSets(value ?? 'all'),
|
||||
returns: 'list of QR sets',
|
||||
namedArgumentList: [],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'set type', [ARGUMENT_TYPE.STRING], false, false, null, ['all', 'global', 'chat'],
|
||||
),
|
||||
],
|
||||
helpString: 'Gets a list of the names of all quick reply sets.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-list',
|
||||
callback: (_, value) => this.listQuickReplies(value),
|
||||
returns: 'list of QRs',
|
||||
namedArgumentList: [],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'set name', [ARGUMENT_TYPE.STRING], true,
|
||||
),
|
||||
],
|
||||
helpString: 'Gets a list of the names of all quick replies in this quick reply set.',
|
||||
}));
|
||||
|
||||
const qrArgs = [
|
||||
new SlashCommandNamedArgument('label', 'text on the button, e.g., label=MyButton', [ARGUMENT_TYPE.STRING]),
|
||||
new SlashCommandNamedArgument('set', 'name of the QR set, e.g., set=PresetName1', [ARGUMENT_TYPE.STRING]),
|
||||
new SlashCommandNamedArgument('hidden', 'whether the button should be hidden, e.g., hidden=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
|
||||
new SlashCommandNamedArgument('startup', 'auto execute on app startup, e.g., startup=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
|
||||
new SlashCommandNamedArgument('user', 'auto execute on user message, e.g., user=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
|
||||
new SlashCommandNamedArgument('bot', 'auto execute on AI message, e.g., bot=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
|
||||
new SlashCommandNamedArgument('load', 'auto execute on chat load, e.g., load=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
|
||||
new SlashCommandNamedArgument('group', 'auto execute on group member selection, e.g., group=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
|
||||
new SlashCommandNamedArgument('title', 'title / tooltip to be shown on button, e.g., title="My Fancy Button"', [ARGUMENT_TYPE.STRING], false),
|
||||
];
|
||||
const qrUpdateArgs = [
|
||||
new SlashCommandNamedArgument('newlabel', 'new text for the button', [ARGUMENT_TYPE.STRING], false),
|
||||
];
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-create',
|
||||
callback: (args, message) => this.createQuickReply(args, message),
|
||||
namedArgumentList: qrArgs,
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'command', [ARGUMENT_TYPE.STRING], true,
|
||||
),
|
||||
],
|
||||
helpString: `
|
||||
<div>Creates a new Quick Reply.</div>
|
||||
<div>
|
||||
<strong>Example:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code>/qr-create set=MyPreset label=MyButton /echo 123</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-update',
|
||||
callback: (args, message) => this.updateQuickReply(args, message),
|
||||
returns: 'updated quick reply',
|
||||
namedArgumentList: [...qrUpdateArgs, ...qrArgs],
|
||||
helpString: `
|
||||
<div>
|
||||
Updates Quick Reply.
|
||||
</div>
|
||||
<div>
|
||||
<strong>Example:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code>/qr-update set=MyPreset label=MyButton newlabel=MyRenamedButton /echo 123</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-delete',
|
||||
callback: (args, name) => this.deleteQuickReply(args, name),
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument(
|
||||
'set', 'Quick Reply set', [ARGUMENT_TYPE.STRING], true,
|
||||
),
|
||||
new SlashCommandNamedArgument(
|
||||
'label', 'Quick Reply label', [ARGUMENT_TYPE.STRING], false,
|
||||
),
|
||||
],
|
||||
helpString: 'Deletes a Quick Reply from the specified set. If no label is provided, the entire set is deleted.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-contextadd',
|
||||
callback: (args, name) => this.createContextItem(args, name),
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument(
|
||||
'set', 'string', [ARGUMENT_TYPE.STRING], true,
|
||||
),
|
||||
new SlashCommandNamedArgument(
|
||||
'label', 'string', [ARGUMENT_TYPE.STRING], true,
|
||||
),
|
||||
new SlashCommandNamedArgument(
|
||||
'chain', 'boolean', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false',
|
||||
),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'preset name', [ARGUMENT_TYPE.STRING], true,
|
||||
),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Add context menu preset to a QR.
|
||||
</div>
|
||||
<div>
|
||||
<strong>Example:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code>/qr-contextadd set=MyPreset label=MyButton chain=true MyOtherPreset</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-contextdel',
|
||||
callback: (args, name) => this.deleteContextItem(args, name),
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument(
|
||||
'set', 'string', [ARGUMENT_TYPE.STRING], true,
|
||||
),
|
||||
new SlashCommandNamedArgument(
|
||||
'label', 'string', [ARGUMENT_TYPE.STRING], true,
|
||||
),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'preset name', [ARGUMENT_TYPE.STRING], true,
|
||||
),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Remove context menu preset from a QR.
|
||||
</div>
|
||||
<div>
|
||||
<strong>Example:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code>/qr-contextdel set=MyPreset label=MyButton MyOtherPreset</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-contextclear',
|
||||
callback: (args, label) => this.clearContextMenu(args, label),
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument(
|
||||
'set', 'context menu preset name', [ARGUMENT_TYPE.STRING], true,
|
||||
),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'label', [ARGUMENT_TYPE.STRING], true,
|
||||
),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Remove all context menu presets from a QR.
|
||||
</div>
|
||||
<div>
|
||||
<strong>Example:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code>/qr-contextclear set=MyPreset MyButton</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
|
||||
const presetArgs = [
|
||||
new SlashCommandNamedArgument('nosend', 'disable send / insert in user input (invalid for slash commands)', [ARGUMENT_TYPE.BOOLEAN], false),
|
||||
new SlashCommandNamedArgument('before', 'place QR before user input', [ARGUMENT_TYPE.BOOLEAN], false),
|
||||
new SlashCommandNamedArgument('inject', 'inject user input automatically (if disabled use {{input}})', [ARGUMENT_TYPE.BOOLEAN], false),
|
||||
];
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-create',
|
||||
callback: (args, name) => this.createSet(name, args),
|
||||
aliases: ['qr-presetadd'],
|
||||
namedArgumentList: presetArgs,
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'name', [ARGUMENT_TYPE.STRING], true,
|
||||
),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Create a new preset (overrides existing ones).
|
||||
</div>
|
||||
<div>
|
||||
<strong>Example:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code>/qr-set-add MyNewPreset</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-update',
|
||||
callback: (args, name) => this.updateSet(name, args),
|
||||
aliases: ['qr-presetupdate'],
|
||||
namedArgumentList: presetArgs,
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument('name', [ARGUMENT_TYPE.STRING], true),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Update an existing preset.
|
||||
</div>
|
||||
<div>
|
||||
<strong>Example:</strong>
|
||||
<pre><code>/qr-set-update enabled=false MyPreset</code></pre>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-delete',
|
||||
callback: (args, name) => this.deleteSet(name),
|
||||
aliases: ['qr-presetdelete'],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument('name', [ARGUMENT_TYPE.STRING], true),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Delete an existing preset.
|
||||
</div>
|
||||
<div>
|
||||
<strong>Example:</strong>
|
||||
<pre><code>/qr-set-delete MyPreset</code></pre>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -209,6 +209,10 @@
|
|||
justify-content: center;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
#qr--qrOptions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
#qr--qrOptions > #qr--ctxEditor .qr--ctxItem {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@ -218,12 +222,16 @@
|
|||
@media screen and (max-width: 750px) {
|
||||
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor {
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
}
|
||||
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels {
|
||||
flex-direction: column;
|
||||
}
|
||||
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-message {
|
||||
min-height: 90svh;
|
||||
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder {
|
||||
min-height: 50svh;
|
||||
}
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) {
|
||||
|
@ -238,11 +246,13 @@
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1em;
|
||||
overflow: hidden;
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels {
|
||||
flex: 0 0 auto;
|
||||
|
@ -268,6 +278,7 @@
|
|||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings {
|
||||
display: flex;
|
||||
|
@ -283,17 +294,167 @@
|
|||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings > .checkbox_label > input {
|
||||
font-size: inherit;
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-message {
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder {
|
||||
flex: 1 1 auto;
|
||||
display: grid;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-execute {
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-messageSyntax {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
overflow: hidden;
|
||||
min-width: 100%;
|
||||
width: 0;
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-messageSyntax > #qr--modal-messageSyntaxInner {
|
||||
height: 100%;
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
caret-color: white;
|
||||
mix-blend-mode: difference;
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message::-webkit-scrollbar,
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #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) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder #qr--modal-message,
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder #qr--modal-messageSyntaxInner {
|
||||
padding: 0.75em;
|
||||
margin: 0;
|
||||
border: none;
|
||||
resize: none;
|
||||
line-height: 1.2;
|
||||
border: 1px solid var(--SmartThemeBorderColor);
|
||||
border-radius: 5px;
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons {
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons .qr--modal-executeButton {
|
||||
border-width: 2px;
|
||||
border-style: solid;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5em;
|
||||
padding: 0.5em 0.75em;
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-execute.qr--busy {
|
||||
opacity: 0.5;
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons .qr--modal-executeButton .qr--modal-executeComboIcon {
|
||||
display: flex;
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-execute {
|
||||
transition: 200ms;
|
||||
filter: grayscale(0);
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-execute.qr--busy {
|
||||
cursor: wait;
|
||||
opacity: 0.5;
|
||||
filter: grayscale(1);
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-execute {
|
||||
border-color: #51a351;
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-pause,
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-stop {
|
||||
cursor: default;
|
||||
opacity: 0.5;
|
||||
filter: grayscale(1);
|
||||
pointer-events: none;
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons .qr--busy ~ #qr--modal-pause,
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons .qr--busy ~ #qr--modal-stop {
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
filter: grayscale(0);
|
||||
pointer-events: all;
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-pause {
|
||||
border-color: #92befc;
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-stop {
|
||||
border-color: #d78872;
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeProgress {
|
||||
--prog: 0;
|
||||
--progColor: #92befc;
|
||||
--progFlashColor: #d78872;
|
||||
--progSuccessColor: #51a351;
|
||||
--progErrorColor: #bd362f;
|
||||
--progAbortedColor: #d78872;
|
||||
height: 0.5em;
|
||||
background-color: var(--black50a);
|
||||
position: relative;
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeProgress:after {
|
||||
content: '';
|
||||
background-color: var(--progColor);
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
right: calc(100% - var(--prog) * 1%);
|
||||
transition: 200ms;
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeProgress.qr--paused:after {
|
||||
animation-name: qr--progressPulse;
|
||||
animation-duration: 1500ms;
|
||||
animation-timing-function: ease-in-out;
|
||||
animation-delay: 0s;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeProgress.qr--aborted:after {
|
||||
background-color: var(--progAbortedColor);
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeProgress.qr--success:after {
|
||||
background-color: var(--progSuccessColor);
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeProgress.qr--error:after {
|
||||
background-color: var(--progErrorColor);
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeErrors {
|
||||
display: none;
|
||||
text-align: left;
|
||||
font-size: smaller;
|
||||
background-color: #bd362f;
|
||||
color: white;
|
||||
padding: 0.5em;
|
||||
overflow: auto;
|
||||
min-width: 100%;
|
||||
width: 0;
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeErrors.qr--hasErrors {
|
||||
display: block;
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeResult {
|
||||
display: none;
|
||||
text-align: left;
|
||||
font-size: smaller;
|
||||
background-color: #51a351;
|
||||
color: white;
|
||||
padding: 0.5em;
|
||||
overflow: auto;
|
||||
min-width: 100%;
|
||||
width: 0;
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeResult.qr--hasResult {
|
||||
display: block;
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeResult:before {
|
||||
content: 'Result: ';
|
||||
}
|
||||
@keyframes qr--progressPulse {
|
||||
0%,
|
||||
100% {
|
||||
background-color: var(--progColor);
|
||||
}
|
||||
50% {
|
||||
background-color: var(--progFlashColor);
|
||||
}
|
||||
}
|
||||
.shadow_popup.qr--hide {
|
||||
opacity: 0 !important;
|
||||
|
|
|
@ -229,6 +229,8 @@
|
|||
|
||||
|
||||
#qr--qrOptions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
> #qr--ctxEditor {
|
||||
.qr--ctxItem {
|
||||
display: flex;
|
||||
|
@ -244,11 +246,15 @@
|
|||
@media screen and (max-width: 750px) {
|
||||
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor {
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
> #qr--main {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
> #qr--main > .qr--labels {
|
||||
flex-direction: column;
|
||||
}
|
||||
> #qr--main > .qr--modal-messageContainer > #qr--modal-message {
|
||||
min-height: 90svh;
|
||||
> #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder {
|
||||
min-height: 50svh;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -264,11 +270,13 @@
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1em;
|
||||
overflow: hidden;
|
||||
|
||||
> #qr--main {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
> .qr--labels {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
|
@ -293,6 +301,7 @@
|
|||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
> .qr--modal-editorSettings {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@ -307,24 +316,169 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
> #qr--modal-message {
|
||||
> #qr--modal-messageHolder {
|
||||
flex: 1 1 auto;
|
||||
display: grid;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
> #qr--modal-messageSyntax {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
overflow: hidden;
|
||||
min-width: 100%;
|
||||
width: 0;
|
||||
> #qr--modal-messageSyntaxInner {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
> #qr--modal-message {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
caret-color: white;
|
||||
mix-blend-mode: difference;
|
||||
&::-webkit-scrollbar, &::-webkit-scrollbar-thumb {
|
||||
visibility: hidden;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
#qr--modal-message, #qr--modal-messageSyntaxInner {
|
||||
padding: 0.75em;
|
||||
margin: 0;
|
||||
border: none;
|
||||
resize: none;
|
||||
line-height: 1.2;
|
||||
border: 1px solid var(--SmartThemeBorderColor);
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#qr--modal-execute {
|
||||
#qr--modal-executeButtons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5em;
|
||||
&.qr--busy {
|
||||
opacity: 0.5;
|
||||
cursor: wait;
|
||||
gap: 1em;
|
||||
.qr--modal-executeButton {
|
||||
border-width: 2px;
|
||||
border-style: solid;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5em;
|
||||
padding: 0.5em 0.75em;
|
||||
.qr--modal-executeComboIcon {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
#qr--modal-execute {
|
||||
transition: 200ms;
|
||||
filter: grayscale(0);
|
||||
&.qr--busy {
|
||||
cursor: wait;
|
||||
opacity: 0.5;
|
||||
filter: grayscale(1);
|
||||
}
|
||||
}
|
||||
#qr--modal-execute {
|
||||
border-color: rgb(81, 163, 81);
|
||||
}
|
||||
#qr--modal-pause, #qr--modal-stop {
|
||||
cursor: default;
|
||||
opacity: 0.5;
|
||||
filter: grayscale(1);
|
||||
pointer-events: none;
|
||||
}
|
||||
.qr--busy {
|
||||
~ #qr--modal-pause, ~ #qr--modal-stop {
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
filter: grayscale(0);
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
#qr--modal-pause {
|
||||
border-color: rgb(146, 190, 252);
|
||||
}
|
||||
#qr--modal-stop {
|
||||
border-color: rgb(215, 136, 114);
|
||||
}
|
||||
}
|
||||
#qr--modal-executeProgress {
|
||||
--prog: 0;
|
||||
--progColor: rgb(146, 190, 252);
|
||||
--progFlashColor: rgb(215, 136, 114);
|
||||
--progSuccessColor: rgb(81, 163, 81);
|
||||
--progErrorColor: rgb(189, 54, 47);
|
||||
--progAbortedColor: rgb(215, 136, 114);
|
||||
height: 0.5em;
|
||||
background-color: var(--black50a);
|
||||
position: relative;
|
||||
&:after {
|
||||
content: '';
|
||||
background-color: var(--progColor);
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
right: calc(100% - var(--prog) * 1%);
|
||||
transition: 200ms;
|
||||
}
|
||||
&.qr--paused:after {
|
||||
animation-name: qr--progressPulse;
|
||||
animation-duration: 1500ms;
|
||||
animation-timing-function: ease-in-out;
|
||||
animation-delay: 0s;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
&.qr--aborted:after {
|
||||
background-color: var(--progAbortedColor);
|
||||
}
|
||||
&.qr--success:after {
|
||||
background-color: var(--progSuccessColor);
|
||||
}
|
||||
&.qr--error:after {
|
||||
background-color: var(--progErrorColor);
|
||||
}
|
||||
}
|
||||
#qr--modal-executeErrors {
|
||||
display: none;
|
||||
&.qr--hasErrors {
|
||||
display: block;
|
||||
}
|
||||
text-align: left;
|
||||
font-size: smaller;
|
||||
background-color: rgb(189, 54, 47);
|
||||
color: white;
|
||||
padding: 0.5em;
|
||||
overflow: auto;
|
||||
min-width: 100%;
|
||||
width: 0;
|
||||
}
|
||||
#qr--modal-executeResult {
|
||||
display: none;
|
||||
&.qr--hasResult {
|
||||
display: block;
|
||||
}
|
||||
&:before { content: 'Result: '; }
|
||||
text-align: left;
|
||||
font-size: smaller;
|
||||
background-color: rgb(81, 163, 81);
|
||||
color: white;
|
||||
padding: 0.5em;
|
||||
overflow: auto;
|
||||
min-width: 100%;
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@keyframes qr--progressPulse {
|
||||
0%, 100% {
|
||||
background-color: var(--progColor);
|
||||
}
|
||||
50% {
|
||||
background-color: var(--progFlashColor);
|
||||
}
|
||||
}
|
||||
|
||||
.shadow_popup.qr--hide {
|
||||
opacity: 0 !important;
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
<i class="fa-solid fa-file-import"></i>
|
||||
<span data-i18n="ext_regex_import_script">Import Script</span>
|
||||
</div>
|
||||
<input type="file" id="import_regex_file" hidden accept="*.json" />
|
||||
<input type="file" id="import_regex_file" hidden accept="*.json" multiple />
|
||||
</div>
|
||||
<hr />
|
||||
<label data-i18n="ext_regex_saved_scripts">Saved Scripts</label>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { substituteParams } from '../../../script.js';
|
||||
import { extension_settings } from '../../extensions.js';
|
||||
import { regexFromString } from '../../utils.js';
|
||||
export {
|
||||
regex_placement,
|
||||
getRegexedString,
|
||||
|
@ -21,29 +22,6 @@ const regex_placement = {
|
|||
WORLD_INFO: 5,
|
||||
};
|
||||
|
||||
/**
|
||||
* Instantiates a regular expression from a string.
|
||||
* @param {string} input The input string.
|
||||
* @returns {RegExp} The regular expression instance.
|
||||
* @copyright Originally from: https://github.com/IonicaBizau/regex-parser.js/blob/master/lib/index.js
|
||||
*/
|
||||
function regexFromString(input) {
|
||||
try {
|
||||
// Parse input
|
||||
var m = input.match(/(\/?)(.+)\1([a-z]*)/i);
|
||||
|
||||
// Invalid flags
|
||||
if (m[3] && !/^(?!.*?(.).*?\1)[gmixXsuUAJ]+$/.test(m[3])) {
|
||||
return RegExp(input);
|
||||
}
|
||||
|
||||
// Create the regular expression
|
||||
return new RegExp(m[2], m[3]);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parent function to fetch a regexed version of a raw string
|
||||
* @param {string} rawString The raw string to be regexed
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { callPopup, getCurrentChatId, reloadCurrentChat, saveSettingsDebounced } from '../../../script.js';
|
||||
import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js';
|
||||
import { registerSlashCommand } from '../../slash-commands.js';
|
||||
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 { download, getFileText, getSortableDelay, uuidv4 } from '../../utils.js';
|
||||
import { resolveVariable } from '../../variables.js';
|
||||
import { regex_placement, runRegexScript } from './engine.js';
|
||||
|
@ -323,7 +325,9 @@ jQuery(async () => {
|
|||
});
|
||||
$('#import_regex_file').on('change', async function () {
|
||||
const inputElement = this instanceof HTMLInputElement && this;
|
||||
await onRegexImportFileChange(inputElement.files[0]);
|
||||
for (const file of inputElement.files) {
|
||||
await onRegexImportFileChange(file);
|
||||
}
|
||||
inputElement.value = '';
|
||||
});
|
||||
$('#import_regex').on('click', function () {
|
||||
|
@ -353,5 +357,20 @@ jQuery(async () => {
|
|||
await loadRegexScripts();
|
||||
$('#saved_regex_scripts').sortable('enable');
|
||||
|
||||
registerSlashCommand('regex', runRegexCallback, [], '(name=scriptName [input]) – runs a Regex extension script by name on the provided string. The script must be enabled.', true, true);
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'regex',
|
||||
callback: runRegexCallback,
|
||||
returns: 'replaced text',
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument(
|
||||
'name', 'script name', [ARGUMENT_TYPE.STRING], true,
|
||||
),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'input', [ARGUMENT_TYPE.STRING], false,
|
||||
),
|
||||
],
|
||||
helpString: 'Runs a Regex extension script by name on the provided string. The script must be enabled.',
|
||||
}));
|
||||
|
||||
});
|
||||
|
|
|
@ -25,7 +25,9 @@ import { getMessageTimeStamp, humanizedDateTime } from '../../RossAscends-mods.j
|
|||
import { SECRET_KEYS, secret_state } from '../../secrets.js';
|
||||
import { getNovelUnlimitedImageGeneration, getNovelAnlas, loadNovelSubscriptionData } from '../../nai-settings.js';
|
||||
import { getMultimodalCaption } from '../shared.js';
|
||||
import { registerSlashCommand } from '../../slash-commands.js';
|
||||
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
||||
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
|
||||
import { resolveVariable } from '../../variables.js';
|
||||
export { MODULE_NAME };
|
||||
|
||||
|
@ -1946,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;
|
||||
}
|
||||
|
||||
|
@ -3055,8 +3057,43 @@ $('#sd_dropdown [id]').on('click', function () {
|
|||
});
|
||||
|
||||
jQuery(async () => {
|
||||
registerSlashCommand('imagine', generatePicture, ['sd', 'img', 'image'], helpString, true, true);
|
||||
registerSlashCommand('imagine-comfy-workflow', changeComfyWorkflow, ['icw'], '(workflowName) - change the workflow to be used for image generation with ComfyUI, e.g. <tt>/imagine-comfy-workflow MyWorkflow</tt>');
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'imagine',
|
||||
callback: generatePicture,
|
||||
aliases: ['sd', 'img', 'image'],
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument(
|
||||
'quiet', 'whether to post the generated image to chat', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false', ['false', 'true'],
|
||||
),
|
||||
new SlashCommandNamedArgument(
|
||||
'negative', 'negative prompt prefix', [ARGUMENT_TYPE.STRING], false, false, '',
|
||||
),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'argument', [ARGUMENT_TYPE.STRING], false, false, null, Object.values(triggerWords).flat(),
|
||||
),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Requests to generate an image and posts it to chat (unless quiet=true argument is specified). Supported arguments: <code>${Object.values(triggerWords).flat().join(', ')}</code>.
|
||||
</div>
|
||||
<div>
|
||||
Anything else would trigger a "free mode" to make generate whatever you prompted. Example: <code>/imagine apple tree</code> would generate a picture of an apple tree. Returns a link to the generated image.
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'imagine-comfy-workflow',
|
||||
callback: changeComfyWorkflow,
|
||||
aliases: ['icw'],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'workflowName', [ARGUMENT_TYPE.STRING], true,
|
||||
),
|
||||
],
|
||||
helpString: '(workflowName) - change the workflow to be used for image generation with ComfyUI, e.g. <pre><code>/imagine-comfy-workflow MyWorkflow</code></pre>',
|
||||
}));
|
||||
|
||||
|
||||
const template = await renderExtensionTemplateAsync('stable-diffusion', 'settings', defaultSettings);
|
||||
$('#extensions_settings').append(template);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { callPopup, main_api } from '../../../script.js';
|
||||
import { getContext } from '../../extensions.js';
|
||||
import { registerSlashCommand } from '../../slash-commands.js';
|
||||
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
||||
import { getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, tokenizers } from '../../tokenizers.js';
|
||||
import { resetScrollHeight, debounce } from '../../utils.js';
|
||||
import { debounce_timeout } from '../../constants.js';
|
||||
|
@ -132,5 +133,10 @@ jQuery(() => {
|
|||
</div>`;
|
||||
$('#extensionsMenu').prepend(buttonHtml);
|
||||
$('#token_counter').on('click', doTokenCounter);
|
||||
registerSlashCommand('count', doCount, [], '– counts the number of tokens in the current chat', true, false);
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'count',
|
||||
callback: doCount,
|
||||
returns: 'number of tokens',
|
||||
helpString: 'Counts the number of tokens in the current chat.',
|
||||
}));
|
||||
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -8,11 +8,13 @@ import { CoquiTtsProvider } from './coqui.js';
|
|||
import { SystemTtsProvider } from './system.js';
|
||||
import { NovelTtsProvider } from './novel.js';
|
||||
import { power_user } from '../../power-user.js';
|
||||
import { registerSlashCommand } from '../../slash-commands.js';
|
||||
import { OpenAITtsProvider } from './openai.js';
|
||||
import { XTTSTtsProvider } from './xtts.js';
|
||||
import { AllTalkTtsProvider } from './alltalk.js';
|
||||
import { SpeechT5TtsProvider } from './speecht5.js';
|
||||
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
||||
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
|
||||
export { talkingAnimation };
|
||||
|
||||
const UPDATE_INTERVAL = 1000;
|
||||
|
@ -259,6 +261,7 @@ async function playAudioData(audioJob) {
|
|||
audioElement.addEventListener('ended', completeCurrentAudioJob);
|
||||
audioElement.addEventListener('canplay', () => {
|
||||
console.debug('Starting TTS playback');
|
||||
audioElement.playbackRate = extension_settings.tts.playback_rate;
|
||||
audioElement.play();
|
||||
});
|
||||
}
|
||||
|
@ -527,6 +530,10 @@ function loadSettings() {
|
|||
$('#tts_pass_asterisks').prop('checked', extension_settings.tts.pass_asterisks);
|
||||
$('#tts_skip_codeblocks').prop('checked', extension_settings.tts.skip_codeblocks);
|
||||
$('#tts_skip_tags').prop('checked', extension_settings.tts.skip_tags);
|
||||
$('#playback_rate').val(extension_settings.tts.playback_rate);
|
||||
$('#playback_rate_counter').val(Number(extension_settings.tts.playback_rate).toFixed(2));
|
||||
$('#playback_rate_block').toggle(extension_settings.tts.currentProvider !== 'System');
|
||||
|
||||
$('body').toggleClass('tts', extension_settings.tts.enabled);
|
||||
}
|
||||
|
||||
|
@ -536,6 +543,7 @@ const defaultSettings = {
|
|||
currentProvider: 'ElevenLabs',
|
||||
auto_generation: true,
|
||||
narrate_user: false,
|
||||
playback_rate: 1,
|
||||
};
|
||||
|
||||
function setTtsStatus(status, success) {
|
||||
|
@ -647,6 +655,7 @@ async function loadTtsProvider(provider) {
|
|||
function onTtsProviderChange() {
|
||||
const ttsProviderSelection = $('#tts_provider').val();
|
||||
extension_settings.tts.currentProvider = ttsProviderSelection;
|
||||
$('#playback_rate_block').toggle(extension_settings.tts.currentProvider !== 'System');
|
||||
loadTtsProvider(ttsProviderSelection);
|
||||
}
|
||||
|
||||
|
@ -1020,6 +1029,20 @@ $(document).ready(function () {
|
|||
<small>Pass Asterisks to TTS Engine</small>
|
||||
</label>
|
||||
</div>
|
||||
<div id="playback_rate_block" class="range-block">
|
||||
<hr>
|
||||
<div class="range-block-title justifyLeft" data-i18n="Audio Playback Speed">
|
||||
<small>Audio Playback Speed</small>
|
||||
</div>
|
||||
<div class="range-block-range-and-counter">
|
||||
<div class="range-block-range">
|
||||
<input type="range" id="playback_rate" name="volume" min="0" max="3" step="0.05">
|
||||
</div>
|
||||
<div class="range-block-counter">
|
||||
<input type="number" min="0" max="3" step="0.05" data-for="playback_rate" id="playback_rate_counter">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="tts_voicemap_block">
|
||||
</div>
|
||||
<hr>
|
||||
|
@ -1044,6 +1067,15 @@ $(document).ready(function () {
|
|||
$('#tts_pass_asterisks').on('click', onPassAsterisksClick);
|
||||
$('#tts_auto_generation').on('click', onAutoGenerationClick);
|
||||
$('#tts_narrate_user').on('click', onNarrateUserClick);
|
||||
|
||||
$('#playback_rate').on('input', function () {
|
||||
const value = $(this).val();
|
||||
const formattedValue = Number(value).toFixed(2);
|
||||
extension_settings.tts.playback_rate = value;
|
||||
$('#playback_rate_counter').val(formattedValue);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#tts_voices').on('click', onTtsVoicesClick);
|
||||
for (const provider in ttsProviders) {
|
||||
$('#tts_provider').append($('<option />').val(provider).text(provider));
|
||||
|
@ -1061,8 +1093,38 @@ $(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);
|
||||
registerSlashCommand('speak', onNarrateText, ['narrate', 'tts'], '<span class="monospace">(text)</span> – narrate any text using currently selected character\'s voice. Use voice="Character Name" argument to set other voice from the voice map, example: <tt>/speak voice="Donald Duck" Quack!</tt>', true, true);
|
||||
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'],
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument(
|
||||
'voice', 'character voice name', [ARGUMENT_TYPE.STRING], false,
|
||||
),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'text', [ARGUMENT_TYPE.STRING], true,
|
||||
),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Narrate any text using currently selected character's voice.
|
||||
</div>
|
||||
<div>
|
||||
Use <code>voice="Character Name"</code> argument to set other voice from the voice map.
|
||||
</div>
|
||||
<div>
|
||||
<strong>Example:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code>/speak voice="Donald Duck" Quack!</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
|
||||
document.body.appendChild(audioElement);
|
||||
});
|
||||
|
|
|
@ -916,6 +916,28 @@ async function onVectorizeAllFilesClick() {
|
|||
const chatAttachments = getContext().chat.filter(x => x.extra?.file).map(x => x.extra.file);
|
||||
const allFiles = [...dataBank, ...chatAttachments];
|
||||
|
||||
/**
|
||||
* Gets the chunk size for a file attachment.
|
||||
* @param file {import('../../chats.js').FileAttachment} File attachment
|
||||
* @returns {number} Chunk size for the file
|
||||
*/
|
||||
function getChunkSize(file) {
|
||||
if (chatAttachments.includes(file)) {
|
||||
// Convert kilobytes to string length
|
||||
const thresholdLength = settings.size_threshold * 1024;
|
||||
return file.size > thresholdLength ? settings.chunk_size : -1;
|
||||
}
|
||||
|
||||
if (dataBank.includes(file)) {
|
||||
// Convert kilobytes to string length
|
||||
const thresholdLength = settings.size_threshold_db * 1024;
|
||||
// Use chunk size from settings if file is larger than threshold
|
||||
return file.size > thresholdLength ? settings.chunk_size_db : -1;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
let allSuccess = true;
|
||||
|
||||
for (const file of allFiles) {
|
||||
|
@ -928,7 +950,8 @@ async function onVectorizeAllFilesClick() {
|
|||
continue;
|
||||
}
|
||||
|
||||
const result = await vectorizeFile(text, file.name, collectionId, settings.chunk_size);
|
||||
const chunkSize = getChunkSize(file);
|
||||
const result = await vectorizeFile(text, file.name, collectionId, chunkSize);
|
||||
|
||||
if (!result) {
|
||||
allSuccess = false;
|
||||
|
|
|
@ -258,9 +258,8 @@ export class FilterHelper {
|
|||
*/
|
||||
folderFilter(data) {
|
||||
const state = this.filterData[FILTER_TYPES.FOLDER];
|
||||
// Slightly different than the other filters, as a positive folder filter means it doesn't filter anything (folders get "not hidden" at another place),
|
||||
// while a negative state should then filter out all folders.
|
||||
const isFolder = entity => isFilterState(state, FILTER_STATES.SELECTED) ? true : entity.type === 'tag';
|
||||
// Filter directly on folder. Special rules on still displaying characters with active folder filter are implemented in 'getEntitiesList' directly.
|
||||
const isFolder = entity => entity.type === 'tag';
|
||||
|
||||
return this.filterDataByState(data, state, isFolder);
|
||||
}
|
||||
|
@ -342,15 +341,40 @@ export class FilterHelper {
|
|||
* Applies all filters to the given data.
|
||||
* @param {any[]} data - The data to filter.
|
||||
* @param {object} options - Optional call parameters
|
||||
* @param {boolean|FilterType} [options.clearScoreCache=true] - Whether the score
|
||||
* @param {boolean} [options.clearScoreCache=true] - Whether the score cache should be cleared.
|
||||
* @param {Object.<FilterType, any>} [options.tempOverrides={}] - Temporarily override specific filters for this filter application
|
||||
* @returns {any[]} The filtered data.
|
||||
*/
|
||||
applyFilters(data, { clearScoreCache = true } = {}) {
|
||||
applyFilters(data, { clearScoreCache = true, tempOverrides = {} } = {}) {
|
||||
if (clearScoreCache) this.clearScoreCache();
|
||||
return Object.values(this.filterFunctions)
|
||||
.reduce((data, fn) => fn(data), data);
|
||||
|
||||
// Save original filter states
|
||||
const originalStates = {};
|
||||
for (const key in tempOverrides) {
|
||||
originalStates[key] = this.filterData[key];
|
||||
this.filterData[key] = tempOverrides[key];
|
||||
}
|
||||
|
||||
try {
|
||||
const result = Object.values(this.filterFunctions)
|
||||
.reduce((data, fn) => fn(data), data);
|
||||
|
||||
// Restore original filter states
|
||||
for (const key in originalStates) {
|
||||
this.filterData[key] = originalStates[key];
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
// Restore original filter states in case of an error
|
||||
for (const key in originalStates) {
|
||||
this.filterData[key] = originalStates[key];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Cache scores for a specific filter type
|
||||
* @param {FilterType} type - The type of data being cached
|
||||
|
|
|
@ -637,7 +637,7 @@ function isValidImageUrl(url) {
|
|||
if (Object.keys(url).length === 0) {
|
||||
return false;
|
||||
}
|
||||
return isDataURL(url) || (url && url.startsWith('user'));
|
||||
return isDataURL(url) || (url && (url.startsWith('user') || url.startsWith('/user')));
|
||||
}
|
||||
|
||||
function getGroupAvatar(group) {
|
||||
|
@ -804,7 +804,7 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
|
|||
const bias = getBiasStrings(userInput, type);
|
||||
await sendMessageAsUser(userInput, bias.messageBias);
|
||||
await saveChatConditional();
|
||||
$('#send_textarea').val('').trigger('input');
|
||||
$('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles:true }));
|
||||
}
|
||||
|
||||
// now the real generation begins: cycle through every activated character
|
||||
|
@ -1418,6 +1418,10 @@ function select_group_chats(groupId, skipAnimation) {
|
|||
* @returns {Promise<void>} - A promise that resolves when the processing and upload is complete.
|
||||
*/
|
||||
async function uploadGroupAvatar(event) {
|
||||
if (!(event.target instanceof HTMLInputElement) || !event.target.files.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const file = event.target.files[0];
|
||||
|
||||
if (!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.
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
power_user,
|
||||
context_presets,
|
||||
} from './power-user.js';
|
||||
import { resetScrollHeight } from './utils.js';
|
||||
import { regexFromString, resetScrollHeight } from './utils.js';
|
||||
|
||||
/**
|
||||
* @type {any[]} Instruct mode presets.
|
||||
|
@ -189,10 +189,10 @@ export function autoSelectInstructPreset(modelId) {
|
|||
// If activation regex is set, check if it matches the model id
|
||||
if (preset.activation_regex) {
|
||||
try {
|
||||
const regex = new RegExp(preset.activation_regex, 'i');
|
||||
const regex = regexFromString(preset.activation_regex);
|
||||
|
||||
// Stop on first match so it won't cycle back and forth between presets if multiple regexes match
|
||||
if (regex.test(modelId)) {
|
||||
if (regex instanceof RegExp && regex.test(modelId)) {
|
||||
selectInstructPreset(preset.name);
|
||||
|
||||
return true;
|
||||
|
|
|
@ -259,6 +259,26 @@ function diceRollReplace(input, invalidRollPlaceholder = '') {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the difference between two times. Works with any time format acceptable by moment().
|
||||
* Can work with {{date}} {{time}} macros
|
||||
* @param {string} input - The string to replace time difference macros in.
|
||||
* @returns {string} The string with replaced time difference macros.
|
||||
*/
|
||||
function timeDiffReplace(input) {
|
||||
const timeDiffPattern = /{{timeDiff::(.*?)::(.*?)}}/gi;
|
||||
|
||||
const output = input.replace(timeDiffPattern, (_match, matchPart1, matchPart2) => {
|
||||
const time1 = moment(matchPart1);
|
||||
const time2 = moment(matchPart2);
|
||||
|
||||
const timeDifference = moment.duration(time1.diff(time2));
|
||||
return timeDifference.humanize();
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Substitutes {{macro}} parameters in a string.
|
||||
* @param {string} content - The string to substitute parameters in.
|
||||
|
@ -327,6 +347,7 @@ export function evaluateMacros(content, env) {
|
|||
const utcTime = moment().utc().utcOffset(utcOffset).format('LT');
|
||||
return utcTime;
|
||||
});
|
||||
content = timeDiffReplace(content);
|
||||
content = bannedWordsReplace(content);
|
||||
content = randomReplace(content);
|
||||
content = pickReplace(content, rawContent);
|
||||
|
|
|
@ -32,7 +32,6 @@ import {
|
|||
this_chid,
|
||||
} from '../script.js';
|
||||
import { selected_group } from './group-chats.js';
|
||||
import { registerSlashCommand } from './slash-commands.js';
|
||||
|
||||
import {
|
||||
chatCompletionDefaultPrompts,
|
||||
|
@ -51,6 +50,7 @@ import {
|
|||
download,
|
||||
getBase64Async,
|
||||
getFileText,
|
||||
getImageSizeFromDataURL,
|
||||
getSortableDelay,
|
||||
isDataURL,
|
||||
parseJsonFile,
|
||||
|
@ -66,6 +66,9 @@ import {
|
|||
} from './instruct-mode.js';
|
||||
import { isMobile } from './RossAscends-mods.js';
|
||||
import { saveLogprobsForActiveMessage } from './logprobs.js';
|
||||
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
|
||||
import { SlashCommand } from './slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';
|
||||
|
||||
export {
|
||||
openai_messages_count,
|
||||
|
@ -263,6 +266,7 @@ const default_settings = {
|
|||
show_external_models: false,
|
||||
proxy_password: '',
|
||||
assistant_prefill: '',
|
||||
assistant_impersonation: '',
|
||||
human_sysprompt_message: default_claude_human_sysprompt_message,
|
||||
use_ai21_tokenizer: false,
|
||||
use_google_tokenizer: false,
|
||||
|
@ -271,6 +275,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,
|
||||
|
@ -338,6 +343,7 @@ const oai_settings = {
|
|||
show_external_models: false,
|
||||
proxy_password: '',
|
||||
assistant_prefill: '',
|
||||
assistant_impersonation: '',
|
||||
human_sysprompt_message: default_claude_human_sysprompt_message,
|
||||
use_ai21_tokenizer: false,
|
||||
use_google_tokenizer: false,
|
||||
|
@ -346,6 +352,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,
|
||||
|
@ -1762,7 +1769,7 @@ async function sendOpenAIRequest(type, messages, signal) {
|
|||
generate_data['human_sysprompt_message'] = substituteParams(oai_settings.human_sysprompt_message);
|
||||
// Don't add a prefill on quiet gens (summarization)
|
||||
if (!isQuiet) {
|
||||
generate_data['assistant_prefill'] = substituteParams(oai_settings.assistant_prefill);
|
||||
generate_data['assistant_prefill'] = isImpersonate ? substituteParams(oai_settings.assistant_impersonation) : substituteParams(oai_settings.assistant_prefill);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1842,6 +1849,8 @@ async function sendOpenAIRequest(type, messages, signal) {
|
|||
generate_data['seed'] = oai_settings.seed;
|
||||
}
|
||||
|
||||
await eventSource.emit(event_types.CHAT_COMPLETION_SETTINGS_READY, generate_data);
|
||||
|
||||
const generate_url = '/api/backends/chat-completions/generate';
|
||||
const response = await fetch(generate_url, {
|
||||
method: 'POST',
|
||||
|
@ -2186,12 +2195,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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2718,8 +2762,10 @@ function loadOpenAISettings(data, settings) {
|
|||
oai_settings.show_external_models = settings.show_external_models ?? default_settings.show_external_models;
|
||||
oai_settings.proxy_password = settings.proxy_password ?? default_settings.proxy_password;
|
||||
oai_settings.assistant_prefill = settings.assistant_prefill ?? default_settings.assistant_prefill;
|
||||
oai_settings.assistant_impersonation = settings.assistant_impersonation ?? default_settings.assistant_impersonation;
|
||||
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;
|
||||
|
@ -2753,10 +2799,14 @@ function loadOpenAISettings(data, settings) {
|
|||
$('#api_url_scale').val(oai_settings.api_url_scale);
|
||||
$('#openai_proxy_password').val(oai_settings.proxy_password);
|
||||
$('#claude_assistant_prefill').val(oai_settings.assistant_prefill);
|
||||
$('#claude_assistant_impersonation').val(oai_settings.assistant_impersonation);
|
||||
$('#claude_human_sysprompt_textarea').val(oai_settings.human_sysprompt_message);
|
||||
$('#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);
|
||||
|
@ -3069,6 +3119,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
|
|||
api_url_scale: settings.api_url_scale,
|
||||
show_external_models: settings.show_external_models,
|
||||
assistant_prefill: settings.assistant_prefill,
|
||||
assistant_impersonation: settings.assistant_impersonation,
|
||||
human_sysprompt_message: settings.human_sysprompt_message,
|
||||
use_ai21_tokenizer: settings.use_ai21_tokenizer,
|
||||
use_google_tokenizer: settings.use_google_tokenizer,
|
||||
|
@ -3077,6 +3128,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,
|
||||
|
@ -3454,6 +3506,7 @@ function onSettingsPresetChange() {
|
|||
show_external_models: ['#openai_show_external_models', 'show_external_models', true],
|
||||
proxy_password: ['#openai_proxy_password', 'proxy_password', false],
|
||||
assistant_prefill: ['#claude_assistant_prefill', 'assistant_prefill', false],
|
||||
assistant_impersonation: ['#claude_assistant_impersonation', 'assistant_impersonation', false],
|
||||
human_sysprompt_message: ['#claude_human_sysprompt_textarea', 'human_sysprompt_message', false],
|
||||
use_ai21_tokenizer: ['#use_ai21_tokenizer', 'use_ai21_tokenizer', true],
|
||||
use_google_tokenizer: ['#use_google_tokenizer', 'use_google_tokenizer', true],
|
||||
|
@ -3462,6 +3515,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],
|
||||
|
@ -3478,6 +3532,11 @@ function onSettingsPresetChange() {
|
|||
preset.names_behavior = character_names_behavior.COMPLETION;
|
||||
}
|
||||
|
||||
// Claude: Assistant Impersonation Prefill = Inherit from Assistant Prefill
|
||||
if (preset.assistant_prefill !== undefined && preset.assistant_impersonation === undefined) {
|
||||
preset.assistant_impersonation = preset.assistant_prefill;
|
||||
}
|
||||
|
||||
const updateInput = (selector, value) => $(selector).val(value).trigger('input');
|
||||
const updateCheckbox = (selector, value) => $(selector).prop('checked', value).trigger('input');
|
||||
|
||||
|
@ -3513,7 +3572,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')) {
|
||||
|
@ -3677,7 +3736,7 @@ async function onModelChange() {
|
|||
if (oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE) {
|
||||
if (oai_settings.max_context_unlocked) {
|
||||
$('#openai_max_context').attr('max', max_1mil);
|
||||
} else if (value === 'gemini-1.5-pro-latest') {
|
||||
} else if (value === 'gemini-1.5-pro-latest' || value.includes('gemini-1.5-flash')) {
|
||||
$('#openai_max_context').attr('max', max_1mil);
|
||||
} else if (value === 'gemini-ultra' || value === 'gemini-1.0-pro-latest' || value === 'gemini-pro' || value === 'gemini-1.0-ultra-latest') {
|
||||
$('#openai_max_context').attr('max', max_32k);
|
||||
|
@ -4237,11 +4296,14 @@ export function isImageInliningSupported() {
|
|||
// gultra just isn't being offered as multimodal, thanks google.
|
||||
const visionSupportedModels = [
|
||||
'gpt-4-vision',
|
||||
'gemini-1.5-flash-latest',
|
||||
'gemini-1.5-flash',
|
||||
'gemini-1.0-pro-vision-latest',
|
||||
'gemini-1.5-pro-latest',
|
||||
'gemini-pro-vision',
|
||||
'claude-3',
|
||||
'gpt-4-turbo',
|
||||
'gpt-4o',
|
||||
];
|
||||
|
||||
switch (oai_settings.chat_completion_source) {
|
||||
|
@ -4383,7 +4445,18 @@ function runProxyCallback(_, value) {
|
|||
return foundName;
|
||||
}
|
||||
|
||||
registerSlashCommand('proxy', runProxyCallback, [], '<span class="monospace">(name)</span> – sets a proxy preset by name');
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'proxy',
|
||||
callback: runProxyCallback,
|
||||
returns: 'current proxy',
|
||||
namedArgumentList: [],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'name', [ARGUMENT_TYPE.STRING], true,
|
||||
),
|
||||
],
|
||||
helpString: 'Sets a proxy preset by name.',
|
||||
}));
|
||||
|
||||
|
||||
$(document).ready(async function () {
|
||||
$('#test_api_button').on('click', testApiConnection);
|
||||
|
@ -4659,6 +4732,11 @@ $(document).ready(async function () {
|
|||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#claude_assistant_impersonation').on('input', function () {
|
||||
oai_settings.assistant_impersonation = String($(this).val());
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#claude_human_sysprompt_textarea').on('input', function () {
|
||||
oai_settings.human_sysprompt_message = String($('#claude_human_sysprompt_textarea').val());
|
||||
saveSettingsDebounced();
|
||||
|
@ -4694,6 +4772,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();
|
||||
|
|
|
@ -35,7 +35,6 @@ import {
|
|||
selectInstructPreset,
|
||||
} from './instruct-mode.js';
|
||||
|
||||
import { registerSlashCommand } from './slash-commands.js';
|
||||
import { getTagsList, tag_map, tags } from './tags.js';
|
||||
import { tokenizers } from './tokenizers.js';
|
||||
import { BIAS_CACHE } from './logit-bias.js';
|
||||
|
@ -43,6 +42,10 @@ import { renderTemplateAsync } from './templates.js';
|
|||
|
||||
import { countOccurrences, debounce, delay, download, getFileText, isOdd, onlyUnique, resetScrollHeight, shuffle, sortMoments, stringToRange, timestampToMoment } from './utils.js';
|
||||
import { FILTER_TYPES } from './filters.js';
|
||||
import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';
|
||||
import { SlashCommand } from './slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';
|
||||
import { AUTOCOMPLETE_WIDTH } from './autocomplete/AutoComplete.js';
|
||||
|
||||
export {
|
||||
loadPowerUserSettings,
|
||||
|
@ -253,6 +256,24 @@ let power_user = {
|
|||
zoomed_avatar_magnification: false,
|
||||
show_tag_filters: false,
|
||||
aux_field: 'character_version',
|
||||
stscript: {
|
||||
matching: 'fuzzy',
|
||||
autocomplete: {
|
||||
autoHide: false,
|
||||
style: 'theme',
|
||||
font: {
|
||||
scale: 0.8,
|
||||
},
|
||||
width: {
|
||||
left: AUTOCOMPLETE_WIDTH.CHAT,
|
||||
right: AUTOCOMPLETE_WIDTH.CHAT,
|
||||
},
|
||||
},
|
||||
parser: {
|
||||
/**@type {Object.<PARSER_FLAG,boolean>} */
|
||||
flags: {},
|
||||
},
|
||||
},
|
||||
restore_user_input: true,
|
||||
reduced_motion: false,
|
||||
compact_input_area: true,
|
||||
|
@ -654,7 +675,7 @@ async function CreateZenSliders(elmnt) {
|
|||
sliderID == 'top_k' ||
|
||||
sliderID == 'mirostat_mode_kobold' ||
|
||||
sliderID == 'rep_pen_range' ||
|
||||
sliderID == 'dry_allowed_length_textgenerationwebui' ||
|
||||
sliderID == 'dry_allowed_length_textgenerationwebui' ||
|
||||
sliderID == 'max_tokens_second_textgenerationwebui') {
|
||||
decimals = 0;
|
||||
}
|
||||
|
@ -662,8 +683,8 @@ async function CreateZenSliders(elmnt) {
|
|||
sliderID == 'max_temp_textgenerationwebui' ||
|
||||
sliderID == 'dynatemp_exponent_textgenerationwebui' ||
|
||||
sliderID == 'smoothing_curve_textgenerationwebui' ||
|
||||
sliderID == 'smoothing_factor_textgenerationwebui') {
|
||||
sliderID == 'dry_multiplier_textgenerationwebui' ||
|
||||
sliderID == 'smoothing_factor_textgenerationwebui' ||
|
||||
sliderID == 'dry_multiplier_textgenerationwebui' ||
|
||||
sliderID == 'dry_base_textgenerationwebui') {
|
||||
decimals = 2;
|
||||
}
|
||||
|
@ -725,7 +746,7 @@ async function CreateZenSliders(elmnt) {
|
|||
sliderID == 'rep_pen_slope' ||
|
||||
sliderID == 'smoothing_factor_textgenerationwebui' ||
|
||||
sliderID == 'smoothing_curve_textgenerationwebui' ||
|
||||
sliderID == 'dry_multiplier_textgenerationwebui' ||
|
||||
sliderID == 'dry_multiplier_textgenerationwebui' ||
|
||||
sliderID == 'min_length_textgenerationwebui') {
|
||||
offVal = 0;
|
||||
}
|
||||
|
@ -1434,11 +1455,32 @@ function getExampleMessagesBehavior() {
|
|||
}
|
||||
|
||||
function loadPowerUserSettings(settings, data) {
|
||||
const defaultStscript = JSON.parse(JSON.stringify(power_user.stscript));
|
||||
// Load from settings.json
|
||||
if (settings.power_user !== undefined) {
|
||||
Object.assign(power_user, settings.power_user);
|
||||
}
|
||||
|
||||
if (power_user.stscript === undefined) {
|
||||
power_user.stscript = defaultStscript;
|
||||
} else {
|
||||
if (power_user.stscript.autocomplete === undefined) {
|
||||
power_user.stscript.autocomplete = defaultStscript.autocomplete;
|
||||
} else {
|
||||
if (power_user.stscript.autocomplete.width === undefined) {
|
||||
power_user.stscript.autocomplete.width = defaultStscript.autocomplete.width;
|
||||
}
|
||||
if (power_user.stscript.autocomplete.font === undefined) {
|
||||
power_user.stscript.autocomplete.font = defaultStscript.autocomplete.font;
|
||||
}
|
||||
}
|
||||
if (power_user.stscript.parser === undefined) {
|
||||
power_user.stscript.parser = defaultStscript.parser;
|
||||
} else if (power_user.stscript.parser.flags === undefined) {
|
||||
power_user.stscript.parser.flags = defaultStscript.parser.flags;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.themes !== undefined) {
|
||||
themes = data.themes;
|
||||
}
|
||||
|
@ -1580,6 +1622,21 @@ function loadPowerUserSettings(settings, data) {
|
|||
$('#chat_width_slider').val(power_user.chat_width);
|
||||
$('#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);
|
||||
$('#stscript_parser_flag_strict_escaping').prop('checked', power_user.stscript.parser.flags[PARSER_FLAG.STRICT_ESCAPING] ?? false);
|
||||
$('#stscript_parser_flag_replace_getvar').prop('checked', power_user.stscript.parser.flags[PARSER_FLAG.REPLACE_GETVAR] ?? false);
|
||||
$('#stscript_autocomplete_font_scale').val(power_user.stscript.autocomplete.font.scale ?? defaultStscript.autocomplete.font.scale);
|
||||
$('#stscript_autocomplete_font_scale_counter').val(power_user.stscript.autocomplete.font.scale ?? defaultStscript.autocomplete.font.scale);
|
||||
document.body.style.setProperty('--ac-font-scale', power_user.stscript.autocomplete.font.scale ?? defaultStscript.autocomplete.font.scale.toString());
|
||||
$('#stscript_autocomplete_width_left').val(power_user.stscript.autocomplete.width.left ?? AUTOCOMPLETE_WIDTH.CHAT);
|
||||
document.querySelector('#stscript_autocomplete_width_left').dispatchEvent(new Event('input', { bubbles:true }));
|
||||
$('#stscript_autocomplete_width_right').val(power_user.stscript.autocomplete.width.right ?? AUTOCOMPLETE_WIDTH.CHAT);
|
||||
document.querySelector('#stscript_autocomplete_width_right').dispatchEvent(new Event('input', { bubbles:true }));
|
||||
|
||||
$('#restore_user_input').prop('checked', power_user.restore_user_input);
|
||||
|
||||
$('#chat_truncation').val(power_user.chat_truncation);
|
||||
|
@ -1869,7 +1926,7 @@ function highlightDefaultContext() {
|
|||
/**
|
||||
* Fuzzy search characters by a search term
|
||||
* @param {string} searchValue - The search term
|
||||
* @returns {{item?: *, refIndex: number, score: number}[]} Results as items with their score
|
||||
* @returns {FuseResult[]} Results as items with their score
|
||||
*/
|
||||
export function fuzzySearchCharacters(searchValue) {
|
||||
// @ts-ignore
|
||||
|
@ -1902,7 +1959,7 @@ export function fuzzySearchCharacters(searchValue) {
|
|||
* Fuzzy search world info entries by a search term
|
||||
* @param {*[]} data - WI items data array
|
||||
* @param {string} searchValue - The search term
|
||||
* @returns {{item?: *, refIndex: number, score: number}[]} Results as items with their score
|
||||
* @returns {FuseResult[]} Results as items with their score
|
||||
*/
|
||||
export function fuzzySearchWorldInfo(data, searchValue) {
|
||||
// @ts-ignore
|
||||
|
@ -1931,7 +1988,7 @@ export function fuzzySearchWorldInfo(data, searchValue) {
|
|||
* Fuzzy search persona entries by a search term
|
||||
* @param {*[]} data - persona data array
|
||||
* @param {string} searchValue - The search term
|
||||
* @returns {{item?: *, refIndex: number, score: number}[]} Results as items with their score
|
||||
* @returns {FuseResult[]} Results as items with their score
|
||||
*/
|
||||
export function fuzzySearchPersonas(data, searchValue) {
|
||||
data = data.map(x => ({ key: x, name: power_user.personas[x] ?? '', description: power_user.persona_descriptions[x]?.description ?? '' }));
|
||||
|
@ -1955,7 +2012,7 @@ export function fuzzySearchPersonas(data, searchValue) {
|
|||
/**
|
||||
* Fuzzy search tags by a search term
|
||||
* @param {string} searchValue - The search term
|
||||
* @returns {{item?: *, refIndex: number, score: number}[]} Results as items with their score
|
||||
* @returns {FuseResult[]} Results as items with their score
|
||||
*/
|
||||
export function fuzzySearchTags(searchValue) {
|
||||
// @ts-ignore
|
||||
|
@ -1977,7 +2034,7 @@ export function fuzzySearchTags(searchValue) {
|
|||
/**
|
||||
* Fuzzy search groups by a search term
|
||||
* @param {string} searchValue - The search term
|
||||
* @returns {{item?: *, refIndex: number, score: number}[]} Results as items with their score
|
||||
* @returns {FuseResult[]} Results as items with their score
|
||||
*/
|
||||
export function fuzzySearchGroups(searchValue) {
|
||||
// @ts-ignore
|
||||
|
@ -3595,6 +3652,69 @@ $(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);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#stscript_autocomplete_style').on('change', function () {
|
||||
const value = $(this).find(':selected').val();
|
||||
power_user.stscript.autocomplete_style = String(value);
|
||||
document.body.setAttribute('data-stscript-style', power_user.stscript.autocomplete_style);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#stscript_autocomplete_font_scale').on('input', function () {
|
||||
const value = $(this).val();
|
||||
$('#stscript_autocomplete_font_scale_counter').val(value);
|
||||
power_user.stscript.autocomplete.font.scale = Number(value);
|
||||
document.body.style.setProperty('--ac-font-scale', value.toString());
|
||||
window.dispatchEvent(new Event('resize', { bubbles:true }));
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
$('#stscript_autocomplete_font_scale_counter').on('input', function () {
|
||||
const value = $(this).val();
|
||||
$('#stscript_autocomplete_font_scale').val(value);
|
||||
power_user.stscript.autocomplete.font.scale = Number(value);
|
||||
document.body.style.setProperty('--ac-font-scale', value.toString());
|
||||
window.dispatchEvent(new Event('resize', { bubbles:true }));
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#stscript_autocomplete_width_left').on('input', function () {
|
||||
const value = $(this).val();
|
||||
power_user.stscript.autocomplete.width.left = Number(value);
|
||||
/**@type {HTMLElement}*/(this.closest('.doubleRangeInputContainer')).style.setProperty('--value', value.toString());
|
||||
window.dispatchEvent(new Event('resize', { bubbles:true }));
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#stscript_autocomplete_width_right').on('input', function () {
|
||||
const value = $(this).val();
|
||||
power_user.stscript.autocomplete.width.right = Number(value);
|
||||
/**@type {HTMLElement}*/(this.closest('.doubleRangeInputContainer')).style.setProperty('--value', value.toString());
|
||||
window.dispatchEvent(new Event('resize', { bubbles:true }));
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#stscript_parser_flag_strict_escaping').on('click', function () {
|
||||
const value = $(this).prop('checked');
|
||||
power_user.stscript.parser.flags[PARSER_FLAG.STRICT_ESCAPING] = value;
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#stscript_parser_flag_replace_getvar').on('click', function () {
|
||||
const value = $(this).prop('checked');
|
||||
power_user.stscript.parser.flags[PARSER_FLAG.REPLACE_GETVAR] = value;
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#restore_user_input').on('input', function () {
|
||||
power_user.restore_user_input = !!$(this).prop('checked');
|
||||
saveSettingsDebounced();
|
||||
|
@ -3673,13 +3793,84 @@ $(document).ready(() => {
|
|||
browser_has_focus = false;
|
||||
});
|
||||
|
||||
registerSlashCommand('vn', toggleWaifu, [], '– swaps Visual Novel Mode On/Off', false, true);
|
||||
registerSlashCommand('newchat', doNewChat, [], '– start a new chat with current character', true, true);
|
||||
registerSlashCommand('random', doRandomChat, [], '<span class="monospace">(optional tag name)</span> – start a new chat with a random character. If an argument is provided, only considers characters that have the specified tag.', true, true);
|
||||
registerSlashCommand('delmode', doDelMode, ['del'], '<span class="monospace">(optional number)</span> – enter message deletion mode, and auto-deletes last N messages if numeric argument is provided', true, true);
|
||||
registerSlashCommand('cut', doMesCut, [], '<span class="monospace">(number or range)</span> – cuts the specified message or continuous chunk from the chat, e.g. <tt>/cut 0-10</tt>. Ranges are inclusive! Returns the text of cut messages separated by a newline.', true, true);
|
||||
registerSlashCommand('resetpanels', doResetPanels, ['resetui'], '– resets UI panels to original state.', true, true);
|
||||
registerSlashCommand('bgcol', setAvgBG, [], '– WIP test of auto-bg avg coloring', true, true);
|
||||
registerSlashCommand('theme', setThemeCallback, [], '<span class="monospace">(name)</span> – sets a UI theme by name', true, true);
|
||||
registerSlashCommand('movingui', setmovingUIPreset, [], '<span class="monospace">(name)</span> – activates a movingUI preset by name', true, true);
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'vn',
|
||||
callback: toggleWaifu,
|
||||
helpString: 'Swaps Visual Novel Mode On/Off',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'newchat',
|
||||
callback: doNewChat,
|
||||
helpString: 'Start a new chat with the current character',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'random',
|
||||
callback: doRandomChat,
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'optional tag name', [ARGUMENT_TYPE.STRING], false,
|
||||
),
|
||||
],
|
||||
helpString: 'Start a new chat with a random character. If an argument is provided, only considers characters that have the specified tag.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'delmode',
|
||||
callback: doDelMode,
|
||||
aliases: ['del'],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'optional number', [ARGUMENT_TYPE.NUMBER], false,
|
||||
),
|
||||
],
|
||||
helpString: 'Enter message deletion mode, and auto-deletes last N messages if numeric argument is provided.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'cut',
|
||||
callback: doMesCut,
|
||||
returns: 'the text of cut messages separated by a newline',
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'number or range', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE], true,
|
||||
),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Cuts the specified message or continuous chunk from the chat.
|
||||
</div>
|
||||
<div>
|
||||
Ranges are inclusive!
|
||||
</div>
|
||||
<div>
|
||||
<strong>Example:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code>/cut 0-10</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
aliases: [],
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'resetpanels',
|
||||
callback: doResetPanels,
|
||||
helpString: 'resets UI panels to original state',
|
||||
aliases: ['resetui'],
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'bgcol',
|
||||
callback: setAvgBG,
|
||||
helpString: '– WIP test of auto-bg avg coloring',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'theme',
|
||||
callback: setThemeCallback,
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'name', [ARGUMENT_TYPE.STRING], true,
|
||||
),
|
||||
],
|
||||
helpString: 'sets a UI theme by name',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'movingui',
|
||||
callback: setmovingUIPreset,
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'name', [ARGUMENT_TYPE.STRING], true,
|
||||
),
|
||||
],
|
||||
helpString: 'activates a movingUI preset by name',
|
||||
}));
|
||||
});
|
||||
|
|
|
@ -20,7 +20,9 @@ import { groups, selected_group } from './group-chats.js';
|
|||
import { instruct_presets } from './instruct-mode.js';
|
||||
import { kai_settings } from './kai-settings.js';
|
||||
import { context_presets, getContextSettings, power_user } from './power-user.js';
|
||||
import { registerSlashCommand } from './slash-commands.js';
|
||||
import { SlashCommand } from './slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';
|
||||
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
|
||||
import {
|
||||
textgenerationwebui_preset_names,
|
||||
textgenerationwebui_presets,
|
||||
|
@ -472,7 +474,33 @@ async function waitForConnection() {
|
|||
export async function initPresetManager() {
|
||||
eventSource.on(event_types.CHAT_CHANGED, autoSelectPreset);
|
||||
registerPresetManagers();
|
||||
registerSlashCommand('preset', presetCommandCallback, [], '<span class="monospace">(name)</span> – sets a preset by name for the current API. Gets the current preset if no name is provided', true, true);
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'preset',
|
||||
callback: presetCommandCallback,
|
||||
returns: 'current preset',
|
||||
namedArgumentList: [],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'name', [ARGUMENT_TYPE.STRING], false,
|
||||
),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Sets a preset by name for the current API. Gets the current preset if no name is provided.
|
||||
</div>
|
||||
<div>
|
||||
<strong>Example:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code>/preset myPreset</code></pre>
|
||||
</li>
|
||||
<li>
|
||||
<pre><code>/preset</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
|
||||
|
||||
$(document).on('click', '[data-preset-manager-update]', async function () {
|
||||
const apiId = $(this).data('preset-manager-update');
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,359 @@
|
|||
import { SlashCommandArgument, SlashCommandNamedArgument } from './SlashCommandArgument.js';
|
||||
import { SlashCommandClosure } from './SlashCommandClosure.js';
|
||||
|
||||
|
||||
|
||||
export class SlashCommand {
|
||||
/**
|
||||
* Creates a SlashCommand from a properties object.
|
||||
* @param {Object} props
|
||||
* @param {string} [props.name]
|
||||
* @param {(namedArguments:Object.<string,string|SlashCommandClosure>, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|void|Promise<string|SlashCommandClosure|void>} [props.callback]
|
||||
* @param {string} [props.helpString]
|
||||
* @param {boolean} [props.splitUnnamedArgument]
|
||||
* @param {string[]} [props.aliases]
|
||||
* @param {string} [props.returns]
|
||||
* @param {SlashCommandNamedArgument[]} [props.namedArgumentList]
|
||||
* @param {SlashCommandArgument[]} [props.unnamedArgumentList]
|
||||
*/
|
||||
static fromProps(props) {
|
||||
const instance = Object.assign(new this(), props);
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**@type {string}*/ name;
|
||||
/**@type {(namedArguments:Object<string, string|SlashCommandClosure>, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|Promise<string|SlashCommandClosure>}*/ callback;
|
||||
/**@type {string}*/ helpString;
|
||||
/**@type {boolean}*/ splitUnnamedArgument = false;
|
||||
/**@type {string[]}*/ aliases = [];
|
||||
/**@type {string}*/ returns;
|
||||
/**@type {SlashCommandNamedArgument[]}*/ namedArgumentList = [];
|
||||
/**@type {SlashCommandArgument[]}*/ unnamedArgumentList = [];
|
||||
|
||||
/**@type {Object.<string, HTMLElement>}*/ helpCache = {};
|
||||
/**@type {Object.<string, DocumentFragment>}*/ helpDetailsCache = {};
|
||||
|
||||
renderHelpItem(key = null) {
|
||||
key = key ?? this.name;
|
||||
if (!this.helpCache[key]) {
|
||||
const typeIcon = '[/]';
|
||||
const li = document.createElement('li'); {
|
||||
li.classList.add('item');
|
||||
const type = document.createElement('span'); {
|
||||
type.classList.add('type');
|
||||
type.classList.add('monospace');
|
||||
type.textContent = typeIcon;
|
||||
li.append(type);
|
||||
}
|
||||
const specs = document.createElement('span'); {
|
||||
specs.classList.add('specs');
|
||||
const name = document.createElement('span'); {
|
||||
name.classList.add('name');
|
||||
name.classList.add('monospace');
|
||||
name.textContent = '/';
|
||||
key.split('').forEach(char=>{
|
||||
const span = document.createElement('span'); {
|
||||
span.textContent = char;
|
||||
name.append(span);
|
||||
}
|
||||
});
|
||||
specs.append(name);
|
||||
}
|
||||
const body = document.createElement('span'); {
|
||||
body.classList.add('body');
|
||||
const args = document.createElement('span'); {
|
||||
args.classList.add('arguments');
|
||||
for (const arg of this.namedArgumentList) {
|
||||
const argItem = document.createElement('span'); {
|
||||
argItem.classList.add('argument');
|
||||
argItem.classList.add('namedArgument');
|
||||
if (!arg.isRequired || (arg.defaultValue ?? false)) argItem.classList.add('optional');
|
||||
if (arg.acceptsMultiple) argItem.classList.add('multiple');
|
||||
const name = document.createElement('span'); {
|
||||
name.classList.add('argument-name');
|
||||
name.textContent = arg.name;
|
||||
argItem.append(name);
|
||||
}
|
||||
if (arg.enumList.length > 0) {
|
||||
const enums = document.createElement('span'); {
|
||||
enums.classList.add('argument-enums');
|
||||
for (const e of arg.enumList) {
|
||||
const enumItem = document.createElement('span'); {
|
||||
enumItem.classList.add('argument-enum');
|
||||
enumItem.textContent = e.value;
|
||||
enums.append(enumItem);
|
||||
}
|
||||
}
|
||||
argItem.append(enums);
|
||||
}
|
||||
} else {
|
||||
const types = document.createElement('span'); {
|
||||
types.classList.add('argument-types');
|
||||
for (const t of arg.typeList) {
|
||||
const type = document.createElement('span'); {
|
||||
type.classList.add('argument-type');
|
||||
type.textContent = t;
|
||||
types.append(type);
|
||||
}
|
||||
}
|
||||
argItem.append(types);
|
||||
}
|
||||
}
|
||||
args.append(argItem);
|
||||
}
|
||||
}
|
||||
for (const arg of this.unnamedArgumentList) {
|
||||
const argItem = document.createElement('span'); {
|
||||
argItem.classList.add('argument');
|
||||
argItem.classList.add('unnamedArgument');
|
||||
if (!arg.isRequired || (arg.defaultValue ?? false)) argItem.classList.add('optional');
|
||||
if (arg.acceptsMultiple) argItem.classList.add('multiple');
|
||||
if (arg.enumList.length > 0) {
|
||||
const enums = document.createElement('span'); {
|
||||
enums.classList.add('argument-enums');
|
||||
for (const e of arg.enumList) {
|
||||
const enumItem = document.createElement('span'); {
|
||||
enumItem.classList.add('argument-enum');
|
||||
enumItem.textContent = e.value;
|
||||
enums.append(enumItem);
|
||||
}
|
||||
}
|
||||
argItem.append(enums);
|
||||
}
|
||||
} else {
|
||||
const types = document.createElement('span'); {
|
||||
types.classList.add('argument-types');
|
||||
for (const t of arg.typeList) {
|
||||
const type = document.createElement('span'); {
|
||||
type.classList.add('argument-type');
|
||||
type.textContent = t;
|
||||
types.append(type);
|
||||
}
|
||||
}
|
||||
argItem.append(types);
|
||||
}
|
||||
}
|
||||
args.append(argItem);
|
||||
}
|
||||
}
|
||||
body.append(args);
|
||||
}
|
||||
const returns = document.createElement('span'); {
|
||||
returns.classList.add('returns');
|
||||
returns.textContent = this.returns ?? 'void';
|
||||
body.append(returns);
|
||||
}
|
||||
specs.append(body);
|
||||
}
|
||||
li.append(specs);
|
||||
}
|
||||
const help = document.createElement('span'); {
|
||||
help.classList.add('help');
|
||||
const content = document.createElement('span'); {
|
||||
content.classList.add('helpContent');
|
||||
content.innerHTML = this.helpString;
|
||||
const text = content.textContent;
|
||||
content.innerHTML = '';
|
||||
content.textContent = text;
|
||||
help.append(content);
|
||||
}
|
||||
li.append(help);
|
||||
}
|
||||
if (this.aliases.length > 0) {
|
||||
const aliases = document.createElement('span'); {
|
||||
aliases.classList.add('aliases');
|
||||
aliases.append(' (alias: ');
|
||||
for (const aliasName of this.aliases) {
|
||||
const alias = document.createElement('span'); {
|
||||
alias.classList.add('monospace');
|
||||
alias.textContent = `/${aliasName}`;
|
||||
aliases.append(alias);
|
||||
}
|
||||
}
|
||||
aliases.append(')');
|
||||
// li.append(aliases);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.helpCache[key] = li;
|
||||
}
|
||||
return /**@type {HTMLElement}*/(this.helpCache[key].cloneNode(true));
|
||||
}
|
||||
|
||||
renderHelpDetails(key = null) {
|
||||
key = key ?? this.name;
|
||||
if (!this.helpDetailsCache[key]) {
|
||||
const frag = document.createDocumentFragment();
|
||||
const cmd = this;
|
||||
const namedArguments = cmd.namedArgumentList ?? [];
|
||||
const unnamedArguments = cmd.unnamedArgumentList ?? [];
|
||||
const returnType = cmd.returns ?? 'void';
|
||||
const helpString = cmd.helpString ?? 'NO DETAILS';
|
||||
const aliasList = [cmd.name, ...(cmd.aliases ?? [])].filter(it=>it != key);
|
||||
const specs = document.createElement('div'); {
|
||||
specs.classList.add('specs');
|
||||
const name = document.createElement('div'); {
|
||||
name.classList.add('name');
|
||||
name.classList.add('monospace');
|
||||
name.title = 'command name';
|
||||
name.textContent = `/${key}`;
|
||||
specs.append(name);
|
||||
}
|
||||
const body = document.createElement('div'); {
|
||||
body.classList.add('body');
|
||||
const args = document.createElement('ul'); {
|
||||
args.classList.add('arguments');
|
||||
for (const arg of namedArguments) {
|
||||
const listItem = document.createElement('li'); {
|
||||
listItem.classList.add('argumentItem');
|
||||
const argSpec = document.createElement('div'); {
|
||||
argSpec.classList.add('argumentSpec');
|
||||
const argItem = document.createElement('div'); {
|
||||
argItem.classList.add('argument');
|
||||
argItem.classList.add('namedArgument');
|
||||
argItem.title = `${arg.isRequired ? '' : 'optional '}named argument`;
|
||||
if (!arg.isRequired || (arg.defaultValue ?? false)) argItem.classList.add('optional');
|
||||
if (arg.acceptsMultiple) argItem.classList.add('multiple');
|
||||
const name = document.createElement('span'); {
|
||||
name.classList.add('argument-name');
|
||||
name.title = `${argItem.title} - name`;
|
||||
name.textContent = arg.name;
|
||||
argItem.append(name);
|
||||
}
|
||||
if (arg.enumList.length > 0) {
|
||||
const enums = document.createElement('span'); {
|
||||
enums.classList.add('argument-enums');
|
||||
enums.title = `${argItem.title} - accepted values`;
|
||||
for (const e of arg.enumList) {
|
||||
const enumItem = document.createElement('span'); {
|
||||
enumItem.classList.add('argument-enum');
|
||||
enumItem.textContent = e.value;
|
||||
enums.append(enumItem);
|
||||
}
|
||||
}
|
||||
argItem.append(enums);
|
||||
}
|
||||
} else {
|
||||
const types = document.createElement('span'); {
|
||||
types.classList.add('argument-types');
|
||||
types.title = `${argItem.title} - accepted types`;
|
||||
for (const t of arg.typeList) {
|
||||
const type = document.createElement('span'); {
|
||||
type.classList.add('argument-type');
|
||||
type.textContent = t;
|
||||
types.append(type);
|
||||
}
|
||||
}
|
||||
argItem.append(types);
|
||||
}
|
||||
}
|
||||
argSpec.append(argItem);
|
||||
}
|
||||
if (arg.defaultValue !== null) {
|
||||
const argDefault = document.createElement('div'); {
|
||||
argDefault.classList.add('argument-default');
|
||||
argDefault.title = 'default value';
|
||||
argDefault.textContent = arg.defaultValue.toString();
|
||||
argSpec.append(argDefault);
|
||||
}
|
||||
}
|
||||
listItem.append(argSpec);
|
||||
}
|
||||
const desc = document.createElement('div'); {
|
||||
desc.classList.add('argument-description');
|
||||
desc.innerHTML = arg.description;
|
||||
listItem.append(desc);
|
||||
}
|
||||
args.append(listItem);
|
||||
}
|
||||
}
|
||||
for (const arg of unnamedArguments) {
|
||||
const listItem = document.createElement('li'); {
|
||||
listItem.classList.add('argumentItem');
|
||||
const argItem = document.createElement('div'); {
|
||||
argItem.classList.add('argument');
|
||||
argItem.classList.add('unnamedArgument');
|
||||
argItem.title = `${arg.isRequired ? '' : 'optional '}unnamed argument`;
|
||||
if (!arg.isRequired || (arg.defaultValue ?? false)) argItem.classList.add('optional');
|
||||
if (arg.acceptsMultiple) argItem.classList.add('multiple');
|
||||
if (arg.enumList.length > 0) {
|
||||
const enums = document.createElement('span'); {
|
||||
enums.classList.add('argument-enums');
|
||||
enums.title = `${argItem.title} - accepted values`;
|
||||
for (const e of arg.enumList) {
|
||||
const enumItem = document.createElement('span'); {
|
||||
enumItem.classList.add('argument-enum');
|
||||
enumItem.textContent = e.value;
|
||||
enums.append(enumItem);
|
||||
}
|
||||
}
|
||||
argItem.append(enums);
|
||||
}
|
||||
} else {
|
||||
const types = document.createElement('span'); {
|
||||
types.classList.add('argument-types');
|
||||
types.title = `${argItem.title} - accepted types`;
|
||||
for (const t of arg.typeList) {
|
||||
const type = document.createElement('span'); {
|
||||
type.classList.add('argument-type');
|
||||
type.textContent = t;
|
||||
types.append(type);
|
||||
}
|
||||
}
|
||||
argItem.append(types);
|
||||
}
|
||||
}
|
||||
listItem.append(argItem);
|
||||
}
|
||||
const desc = document.createElement('div'); {
|
||||
desc.classList.add('argument-description');
|
||||
desc.innerHTML = arg.description;
|
||||
listItem.append(desc);
|
||||
}
|
||||
args.append(listItem);
|
||||
}
|
||||
}
|
||||
body.append(args);
|
||||
}
|
||||
const returns = document.createElement('span'); {
|
||||
returns.classList.add('returns');
|
||||
returns.title = [null, undefined, 'void'].includes(returnType) ? 'command does not return anything' : 'return value';
|
||||
returns.textContent = returnType ?? 'void';
|
||||
body.append(returns);
|
||||
}
|
||||
specs.append(body);
|
||||
}
|
||||
frag.append(specs);
|
||||
}
|
||||
const help = document.createElement('span'); {
|
||||
help.classList.add('help');
|
||||
help.innerHTML = helpString;
|
||||
for (const code of help.querySelectorAll('pre > code')) {
|
||||
code.classList.add('language-stscript');
|
||||
hljs.highlightElement(code);
|
||||
}
|
||||
frag.append(help);
|
||||
}
|
||||
if (aliasList.length > 0) {
|
||||
const aliases = document.createElement('span'); {
|
||||
aliases.classList.add('aliases');
|
||||
for (const aliasName of aliasList) {
|
||||
const alias = document.createElement('span'); {
|
||||
alias.classList.add('alias');
|
||||
alias.textContent = `/${aliasName}`;
|
||||
aliases.append(alias);
|
||||
}
|
||||
}
|
||||
frag.append(aliases);
|
||||
}
|
||||
}
|
||||
this.helpDetailsCache[key] = frag;
|
||||
}
|
||||
const frag = document.createDocumentFragment();
|
||||
frag.append(this.helpDetailsCache[key].cloneNode(true));
|
||||
return frag;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
export class SlashCommandAbortController {
|
||||
/**@type {SlashCommandAbortSignal}*/ signal;
|
||||
|
||||
|
||||
constructor() {
|
||||
this.signal = new SlashCommandAbortSignal();
|
||||
}
|
||||
abort(reason = 'No reason.') {
|
||||
this.signal.aborted = true;
|
||||
this.signal.reason = reason;
|
||||
}
|
||||
pause(reason = 'No reason.') {
|
||||
this.signal.paused = true;
|
||||
this.signal.reason = reason;
|
||||
}
|
||||
continue(reason = 'No reason.') {
|
||||
this.signal.paused = false;
|
||||
this.signal.reason = reason;
|
||||
}
|
||||
}
|
||||
|
||||
export class SlashCommandAbortSignal {
|
||||
/**@type {boolean}*/ paused = false;
|
||||
/**@type {boolean}*/ aborted = false;
|
||||
/**@type {string}*/ reason = null;
|
||||
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
import { SlashCommandClosure } from './SlashCommandClosure.js';
|
||||
import { SlashCommandEnumValue } from './SlashCommandEnumValue.js';
|
||||
|
||||
|
||||
|
||||
/**@readonly*/
|
||||
/**@enum {string}*/
|
||||
export const ARGUMENT_TYPE = {
|
||||
'STRING': 'string',
|
||||
'NUMBER': 'number',
|
||||
'RANGE': 'range',
|
||||
'BOOLEAN': 'bool',
|
||||
'VARIABLE_NAME': 'varname',
|
||||
'CLOSURE': 'closure',
|
||||
'SUBCOMMAND': 'subcommand',
|
||||
'LIST': 'list',
|
||||
'DICTIONARY': 'dictionary',
|
||||
};
|
||||
|
||||
|
||||
|
||||
export class SlashCommandArgument {
|
||||
/**
|
||||
* Creates an unnamed argument from a poperties object.
|
||||
* @param {Object} props
|
||||
* @param {string} props.description description of the argument
|
||||
* @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} props.typeList default: ARGUMENT_TYPE.STRING - list of accepted types (from ARGUMENT_TYPE)
|
||||
* @param {boolean} [props.isRequired] default: false - whether the argument is required (false = optional argument)
|
||||
* @param {boolean} [props.acceptsMultiple] default: false - whether argument accepts multiple values
|
||||
* @param {string|SlashCommandClosure} [props.defaultValue] default value if no value is provided
|
||||
* @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [props.enumList] list of accepted values
|
||||
*/
|
||||
static fromProps(props) {
|
||||
return new SlashCommandArgument(
|
||||
props.description,
|
||||
props.typeList ?? [ARGUMENT_TYPE.STRING],
|
||||
props.isRequired ?? false,
|
||||
props.acceptsMultiple ?? false,
|
||||
props.defaultValue ?? null,
|
||||
props.enumList ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**@type {string}*/ description;
|
||||
/**@type {ARGUMENT_TYPE[]}*/ typeList = [];
|
||||
/**@type {boolean}*/ isRequired = false;
|
||||
/**@type {boolean}*/ acceptsMultiple = false;
|
||||
/**@type {string|SlashCommandClosure}*/ defaultValue;
|
||||
/**@type {SlashCommandEnumValue[]}*/ enumList = [];
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} description
|
||||
* @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} types
|
||||
* @param {string|SlashCommandClosure} defaultValue
|
||||
* @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} enums
|
||||
*/
|
||||
constructor(description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = []) {
|
||||
this.description = description;
|
||||
this.typeList = types ? Array.isArray(types) ? types : [types] : [];
|
||||
this.isRequired = isRequired ?? false;
|
||||
this.acceptsMultiple = acceptsMultiple ?? false;
|
||||
this.defaultValue = defaultValue;
|
||||
this.enumList = (enums ? Array.isArray(enums) ? enums : [enums] : []).map(it=>{
|
||||
if (it instanceof SlashCommandEnumValue) return it;
|
||||
return new SlashCommandEnumValue(it);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export class SlashCommandNamedArgument extends SlashCommandArgument {
|
||||
/**
|
||||
* Creates an unnamed argument from a poperties object.
|
||||
* @param {Object} props
|
||||
* @param {string} props.name the argument's name
|
||||
* @param {string[]} [props.aliasList] list of aliases
|
||||
* @param {string} props.description description of the argument
|
||||
* @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} props.typeList default: ARGUMENT_TYPE.STRING - list of accepted types (from ARGUMENT_TYPE)
|
||||
* @param {boolean} [props.isRequired] default: false - whether the argument is required (false = optional argument)
|
||||
* @param {boolean} [props.acceptsMultiple] default: false - whether argument accepts multiple values
|
||||
* @param {string|SlashCommandClosure} [props.defaultValue] default value if no value is provided
|
||||
* @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [props.enumList] list of accepted values
|
||||
*/
|
||||
static fromProps(props) {
|
||||
return new SlashCommandNamedArgument(
|
||||
props.name,
|
||||
props.description,
|
||||
props.typeList ?? [ARGUMENT_TYPE.STRING],
|
||||
props.isRequired ?? false,
|
||||
props.acceptsMultiple ?? false,
|
||||
props.defaultValue ?? null,
|
||||
props.enumList ?? [],
|
||||
props.aliasList ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**@type {string}*/ name;
|
||||
/**@type {string[]}*/ aliasList = [];
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {string} description
|
||||
* @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} types
|
||||
* @param {string|SlashCommandClosure} defaultValue
|
||||
* @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} enums
|
||||
*/
|
||||
constructor(name, description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = [], aliases = []) {
|
||||
super(description, types, isRequired, acceptsMultiple, defaultValue, enums);
|
||||
this.name = name;
|
||||
this.aliasList = aliases ? Array.isArray(aliases) ? aliases : [aliases] : [];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,179 @@
|
|||
import { AutoCompleteNameResult } from '../autocomplete/AutoCompleteNameResult.js';
|
||||
import { AutoCompleteOption } from '../autocomplete/AutoCompleteOption.js';
|
||||
import { AutoCompleteSecondaryNameResult } from '../autocomplete/AutoCompleteSecondaryNameResult.js';
|
||||
import { SlashCommand } from './SlashCommand.js';
|
||||
import { SlashCommandNamedArgument } from './SlashCommandArgument.js';
|
||||
import { SlashCommandClosure } from './SlashCommandClosure.js';
|
||||
import { SlashCommandCommandAutoCompleteOption } from './SlashCommandCommandAutoCompleteOption.js';
|
||||
import { SlashCommandEnumAutoCompleteOption } from './SlashCommandEnumAutoCompleteOption.js';
|
||||
import { SlashCommandExecutor } from './SlashCommandExecutor.js';
|
||||
import { SlashCommandNamedArgumentAutoCompleteOption } from './SlashCommandNamedArgumentAutoCompleteOption.js';
|
||||
|
||||
export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
|
||||
/**@type {SlashCommandExecutor}*/ executor;
|
||||
|
||||
/**
|
||||
* @param {SlashCommandExecutor} executor
|
||||
* @param {Object.<string,SlashCommand>} commands
|
||||
*/
|
||||
constructor(executor, commands) {
|
||||
super(
|
||||
executor.name,
|
||||
executor.start,
|
||||
Object
|
||||
.keys(commands)
|
||||
.map(key=>new SlashCommandCommandAutoCompleteOption(commands[key], key))
|
||||
,
|
||||
false,
|
||||
()=>`No matching slash commands for "/${this.name}"`,
|
||||
()=>'No slash commands found!',
|
||||
);
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
getSecondaryNameAt(text, index, isSelect) {
|
||||
const namedResult = this.getNamedArgumentAt(text, index, isSelect);
|
||||
if (!namedResult || namedResult.optionList.length == 0 || !namedResult.isRequired) {
|
||||
const unnamedResult = this.getUnnamedArgumentAt(text, index, isSelect);
|
||||
if (!namedResult) return unnamedResult;
|
||||
if (namedResult && unnamedResult) {
|
||||
const combinedResult = new AutoCompleteSecondaryNameResult(
|
||||
namedResult.name,
|
||||
namedResult.start,
|
||||
[...namedResult.optionList, ...unnamedResult.optionList],
|
||||
);
|
||||
combinedResult.isRequired = namedResult.isRequired || unnamedResult.isRequired;
|
||||
return combinedResult;
|
||||
}
|
||||
}
|
||||
return namedResult;
|
||||
}
|
||||
|
||||
getNamedArgumentAt(text, index, isSelect) {
|
||||
function getSplitRegex() {
|
||||
try {
|
||||
return new RegExp('(?<==)');
|
||||
} catch {
|
||||
// For browsers that don't support lookbehind
|
||||
return new RegExp('=(.*)');
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(this.executor.command?.namedArgumentList)) {
|
||||
return null;
|
||||
}
|
||||
const notProvidedNamedArguments = this.executor.command.namedArgumentList.filter(arg=>!this.executor.namedArgumentList.find(it=>it.name == arg.name));
|
||||
let name;
|
||||
let value;
|
||||
let start;
|
||||
let cmdArg;
|
||||
let argAssign;
|
||||
const unamedArgLength = this.executor.endUnnamedArgs - this.executor.startUnnamedArgs;
|
||||
const namedArgsFollowedBySpace = text[this.executor.endNamedArgs] == ' ';
|
||||
if (this.executor.startNamedArgs <= index && this.executor.endNamedArgs + (namedArgsFollowedBySpace ? 1 : 0) >= index) {
|
||||
// cursor is somewhere within the named arguments (including final space)
|
||||
argAssign = this.executor.namedArgumentList.find(it=>it.start <= index && it.end >= index);
|
||||
if (argAssign) {
|
||||
const [argName, ...v] = text.slice(argAssign.start, index).split(getSplitRegex());
|
||||
name = argName;
|
||||
value = v.join('');
|
||||
start = argAssign.start;
|
||||
cmdArg = this.executor.command.namedArgumentList.find(it=>[it.name, `${it.name}=`].includes(argAssign.name));
|
||||
if (cmdArg) notProvidedNamedArguments.push(cmdArg);
|
||||
} else {
|
||||
name = '';
|
||||
start = index;
|
||||
}
|
||||
} else if (unamedArgLength > 0 && index >= this.executor.startUnnamedArgs && index <= this.executor.endUnnamedArgs) {
|
||||
// cursor is somewhere within the unnamed arguments
|
||||
//TODO if index is in first array item and that is a string, treat it as an unfinished named arg
|
||||
if (typeof this.executor.unnamedArgumentList[0].value == 'string') {
|
||||
if (index <= this.executor.startUnnamedArgs + this.executor.unnamedArgumentList[0].value.length) {
|
||||
name = this.executor.unnamedArgumentList[0].value.slice(0, index - this.executor.startUnnamedArgs);
|
||||
start = this.executor.startUnnamedArgs;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name.includes('=') && cmdArg) {
|
||||
// if cursor is already behind "=" check for enums
|
||||
/**@type {SlashCommandNamedArgument} */
|
||||
if (cmdArg && cmdArg.enumList?.length) {
|
||||
if (isSelect && cmdArg.enumList.includes(value) && argAssign && argAssign.end == index) {
|
||||
return null;
|
||||
}
|
||||
const result = new AutoCompleteSecondaryNameResult(
|
||||
value,
|
||||
start + name.length,
|
||||
cmdArg.enumList.map(it=>new SlashCommandEnumAutoCompleteOption(this.executor.command, it)),
|
||||
true,
|
||||
);
|
||||
result.isRequired = true;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (notProvidedNamedArguments.length > 0) {
|
||||
const result = new AutoCompleteSecondaryNameResult(
|
||||
name,
|
||||
start,
|
||||
notProvidedNamedArguments.map(it=>new SlashCommandNamedArgumentAutoCompleteOption(it, this.executor.command)),
|
||||
false,
|
||||
);
|
||||
result.isRequired = notProvidedNamedArguments.find(it=>it.isRequired) != null;
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
getUnnamedArgumentAt(text, index, isSelect) {
|
||||
if (!Array.isArray(this.executor.command?.unnamedArgumentList)) {
|
||||
return null;
|
||||
}
|
||||
const lastArgIsBlank = this.executor.unnamedArgumentList.slice(-1)[0]?.value == '';
|
||||
const notProvidedArguments = this.executor.command.unnamedArgumentList.slice(this.executor.unnamedArgumentList.length - (lastArgIsBlank ? 1 : 0));
|
||||
let value;
|
||||
let start;
|
||||
let cmdArg;
|
||||
let argAssign;
|
||||
if (this.executor.startUnnamedArgs <= index && this.executor.endUnnamedArgs + 1 >= index) {
|
||||
// cursor is somwehere in the unnamed args
|
||||
const idx = this.executor.unnamedArgumentList.findIndex(it=>it.start <= index && it.end >= index);
|
||||
if (idx > -1) {
|
||||
argAssign = this.executor.unnamedArgumentList[idx];
|
||||
cmdArg = this.executor.command.unnamedArgumentList[idx];
|
||||
if (cmdArg && cmdArg.enumList.length > 0) {
|
||||
value = argAssign.value.toString().slice(0, index - argAssign.start);
|
||||
start = argAssign.start;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
value = '';
|
||||
start = index;
|
||||
cmdArg = notProvidedArguments[0];
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (cmdArg == null || cmdArg.enumList.length == 0) return null;
|
||||
|
||||
const result = new AutoCompleteSecondaryNameResult(
|
||||
value,
|
||||
start,
|
||||
cmdArg.enumList.map(it=>new SlashCommandEnumAutoCompleteOption(this.executor.command, it)),
|
||||
false,
|
||||
);
|
||||
const isCompleteValue = cmdArg.enumList.find(it=>it.value == value);
|
||||
const isSelectedValue = isSelect && isCompleteValue;
|
||||
result.isRequired = cmdArg.isRequired && !isSelectedValue && !isCompleteValue;
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
import { escapeRegex } from '../utils.js';
|
||||
import { SlashCommand } from './SlashCommand.js';
|
||||
import { SlashCommandParser } from './SlashCommandParser.js';
|
||||
|
||||
export class SlashCommandBrowser {
|
||||
/**@type {SlashCommand[]}*/ cmdList;
|
||||
/**@type {HTMLElement}*/ dom;
|
||||
/**@type {HTMLElement}*/ search;
|
||||
/**@type {HTMLElement}*/ details;
|
||||
/**@type {Object.<string,HTMLElement>}*/ itemMap = {};
|
||||
/**@type {MutationObserver}*/ mo;
|
||||
|
||||
renderInto(parent) {
|
||||
if (!this.dom) {
|
||||
const queryRegex = /(?:(?:^|\s+)([^\s"][^\s]*?)(?:\s+|$))|(?:(?:^|\s+)"(.*?)(?:"|$)(?:\s+|$))/;
|
||||
const root = document.createElement('div'); {
|
||||
this.dom = root;
|
||||
const search = document.createElement('div'); {
|
||||
search.classList.add('search');
|
||||
const lbl = document.createElement('label'); {
|
||||
lbl.classList.add('searchLabel');
|
||||
lbl.textContent = 'Search: ';
|
||||
const inp = document.createElement('input'); {
|
||||
this.search = inp;
|
||||
inp.classList.add('searchInput');
|
||||
inp.classList.add('text_pole');
|
||||
inp.type = 'search';
|
||||
inp.placeholder = 'Search slash commands - use quotes to search "literal" instead of fuzzy';
|
||||
inp.addEventListener('input', ()=>{
|
||||
this.details?.remove();
|
||||
this.details = null;
|
||||
let query = inp.value.trim();
|
||||
if (query.slice(-1) == '"' && !/(?:^|\s+)"/.test(query)) {
|
||||
query = `"${query}`;
|
||||
}
|
||||
let fuzzyList = [];
|
||||
let quotedList = [];
|
||||
while (query.length > 0) {
|
||||
const match = queryRegex.exec(query);
|
||||
if (!match) break;
|
||||
if (match[1] !== undefined) {
|
||||
fuzzyList.push(new RegExp(`^(.*?)${match[1].split('').map(char=>`(${escapeRegex(char)})`).join('(.*?)')}(.*?)$`, 'i'));
|
||||
} else if (match[2] !== undefined) {
|
||||
quotedList.push(match[2]);
|
||||
}
|
||||
query = query.slice(match.index + match[0].length);
|
||||
}
|
||||
for (const cmd of this.cmdList) {
|
||||
const targets = [
|
||||
cmd.name,
|
||||
...cmd.namedArgumentList.map(it=>it.name),
|
||||
...cmd.namedArgumentList.map(it=>it.description),
|
||||
...cmd.namedArgumentList.map(it=>it.enumList.map(e=>e.value)).flat(),
|
||||
...cmd.namedArgumentList.map(it=>it.typeList).flat(),
|
||||
...cmd.unnamedArgumentList.map(it=>it.description),
|
||||
...cmd.unnamedArgumentList.map(it=>it.enumList.map(e=>e.value)).flat(),
|
||||
...cmd.unnamedArgumentList.map(it=>it.typeList).flat(),
|
||||
...cmd.aliases,
|
||||
cmd.helpString,
|
||||
];
|
||||
const find = ()=>targets.find(t=>(fuzzyList.find(f=>f.test(t)) ?? quotedList.find(q=>t.includes(q))) !== undefined) !== undefined;
|
||||
if (fuzzyList.length + quotedList.length == 0 || find()) {
|
||||
this.itemMap[cmd.name].classList.remove('isFiltered');
|
||||
} else {
|
||||
this.itemMap[cmd.name].classList.add('isFiltered');
|
||||
}
|
||||
}
|
||||
});
|
||||
lbl.append(inp);
|
||||
}
|
||||
search.append(lbl);
|
||||
}
|
||||
root.append(search);
|
||||
}
|
||||
const container = document.createElement('div'); {
|
||||
container.classList.add('commandContainer');
|
||||
const list = document.createElement('div'); {
|
||||
list.classList.add('autoComplete');
|
||||
this.cmdList = Object
|
||||
.keys(SlashCommandParser.commands)
|
||||
.filter(key => SlashCommandParser.commands[key].name == key) // exclude aliases
|
||||
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
|
||||
.map(key => SlashCommandParser.commands[key])
|
||||
;
|
||||
for (const cmd of this.cmdList) {
|
||||
const item = cmd.renderHelpItem();
|
||||
this.itemMap[cmd.name] = item;
|
||||
let details;
|
||||
item.addEventListener('click', ()=>{
|
||||
if (!details) {
|
||||
details = document.createElement('div'); {
|
||||
details.classList.add('autoComplete-detailsWrap');
|
||||
const inner = document.createElement('div'); {
|
||||
inner.classList.add('autoComplete-details');
|
||||
inner.append(cmd.renderHelpDetails());
|
||||
details.append(inner);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.details != details) {
|
||||
Array.from(list.querySelectorAll('.selected')).forEach(it=>it.classList.remove('selected'));
|
||||
item.classList.add('selected');
|
||||
this.details?.remove();
|
||||
container.append(details);
|
||||
this.details = details;
|
||||
const pRect = list.getBoundingClientRect();
|
||||
const rect = item.children[0].getBoundingClientRect();
|
||||
details.style.setProperty('--targetOffset', rect.top - pRect.top);
|
||||
} else {
|
||||
item.classList.remove('selected');
|
||||
details.remove();
|
||||
this.details = null;
|
||||
}
|
||||
});
|
||||
list.append(item);
|
||||
}
|
||||
container.append(list);
|
||||
}
|
||||
root.append(container);
|
||||
}
|
||||
root.classList.add('slashCommandBrowser');
|
||||
}
|
||||
}
|
||||
parent.append(this.dom);
|
||||
|
||||
this.mo = new MutationObserver(muts=>{
|
||||
if (muts.find(mut=>Array.from(mut.removedNodes).find(it=>it == this.dom || it.contains(this.dom)))) {
|
||||
this.mo.disconnect();
|
||||
window.removeEventListener('keydown', boundHandler);
|
||||
}
|
||||
});
|
||||
this.mo.observe(document.querySelector('#chat'), { childList:true, subtree:true });
|
||||
const boundHandler = this.handleKeyDown.bind(this);
|
||||
window.addEventListener('keydown', boundHandler);
|
||||
return this.dom;
|
||||
}
|
||||
|
||||
handleKeyDown(evt) {
|
||||
if (!evt.shiftKey && !evt.altKey && evt.ctrlKey && evt.key.toLowerCase() == 'f') {
|
||||
if (!this.dom.closest('body')) return;
|
||||
if (this.dom.closest('.mes') && !this.dom.closest('.last_mes')) return;
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
evt.stopImmediatePropagation();
|
||||
this.search.focus();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,261 @@
|
|||
import { substituteParams } from '../../script.js';
|
||||
import { delay, escapeRegex } from '../utils.js';
|
||||
import { SlashCommandAbortController } from './SlashCommandAbortController.js';
|
||||
import { SlashCommandClosureExecutor } from './SlashCommandClosureExecutor.js';
|
||||
import { SlashCommandClosureResult } from './SlashCommandClosureResult.js';
|
||||
import { SlashCommandExecutor } from './SlashCommandExecutor.js';
|
||||
import { SlashCommandNamedArgumentAssignment } from './SlashCommandNamedArgumentAssignment.js';
|
||||
import { SlashCommandScope } from './SlashCommandScope.js';
|
||||
|
||||
export class SlashCommandClosure {
|
||||
/**@type {SlashCommandScope}*/ scope;
|
||||
/**@type {boolean}*/ executeNow = false;
|
||||
// @ts-ignore
|
||||
/**@type {SlashCommandNamedArgumentAssignment[]}*/ argumentList = [];
|
||||
// @ts-ignore
|
||||
/**@type {SlashCommandNamedArgumentAssignment[]}*/ providedArgumentList = [];
|
||||
/**@type {SlashCommandExecutor[]}*/ executorList = [];
|
||||
/**@type {SlashCommandAbortController}*/ abortController;
|
||||
/**@type {(done:number, total:number)=>void}*/ onProgress;
|
||||
|
||||
/**@type {number}*/
|
||||
get commandCount() {
|
||||
return this.executorList.map(executor=>executor.commandCount).reduce((sum,cur)=>sum + cur, 0);
|
||||
}
|
||||
|
||||
constructor(parent) {
|
||||
this.scope = new SlashCommandScope(parent);
|
||||
}
|
||||
|
||||
toString() {
|
||||
return '[Closure]';
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} text
|
||||
* @param {SlashCommandScope} scope
|
||||
* @returns
|
||||
*/
|
||||
substituteParams(text, scope = null) {
|
||||
let isList = false;
|
||||
let listValues = [];
|
||||
scope = scope ?? this.scope;
|
||||
const macros = scope.macroList.map(it=>escapeRegex(it.key)).join('|');
|
||||
const re = new RegExp(`({{pipe}})|(?:{{var::([^\\s]+?)(?:::((?!}}).+))?}})|(?:{{(${macros})}})`);
|
||||
let done = '';
|
||||
let remaining = text;
|
||||
while (re.test(remaining)) {
|
||||
const match = re.exec(remaining);
|
||||
const before = substituteParams(remaining.slice(0, match.index));
|
||||
const after = remaining.slice(match.index + match[0].length);
|
||||
const replacer = match[1] ? scope.pipe : match[2] ? scope.getVariable(match[2], match[3]) : scope.macroList.find(it=>it.key == match[4])?.value;
|
||||
if (replacer instanceof SlashCommandClosure) {
|
||||
isList = true;
|
||||
if (match.index > 0) {
|
||||
listValues.push(before);
|
||||
}
|
||||
listValues.push(replacer);
|
||||
if (match.index + match[0].length + 1 < remaining.length) {
|
||||
const rest = this.substituteParams(after, scope);
|
||||
listValues.push(...(Array.isArray(rest) ? rest : [rest]));
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
done = `${done}${before}${replacer}`;
|
||||
remaining = after;
|
||||
}
|
||||
}
|
||||
if (!isList) {
|
||||
text = `${done}${substituteParams(remaining)}`;
|
||||
}
|
||||
|
||||
if (isList) {
|
||||
if (listValues.length > 1) return listValues;
|
||||
return listValues[0];
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
getCopy() {
|
||||
const closure = new SlashCommandClosure();
|
||||
closure.scope = this.scope.getCopy();
|
||||
closure.executeNow = this.executeNow;
|
||||
closure.argumentList = this.argumentList;
|
||||
closure.providedArgumentList = this.providedArgumentList;
|
||||
closure.executorList = this.executorList;
|
||||
closure.abortController = this.abortController;
|
||||
closure.onProgress = this.onProgress;
|
||||
return closure;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns Promise<SlashCommandClosureResult>
|
||||
*/
|
||||
async execute() {
|
||||
const closure = this.getCopy();
|
||||
return await closure.executeDirect();
|
||||
}
|
||||
|
||||
async executeDirect() {
|
||||
// closure arguments
|
||||
for (const arg of this.argumentList) {
|
||||
let v = arg.value;
|
||||
if (v instanceof SlashCommandClosure) {
|
||||
/**@type {SlashCommandClosure}*/
|
||||
const closure = v;
|
||||
closure.scope.parent = this.scope;
|
||||
if (closure.executeNow) {
|
||||
v = (await closure.execute())?.pipe;
|
||||
} else {
|
||||
v = closure;
|
||||
}
|
||||
} else {
|
||||
v = this.substituteParams(v);
|
||||
}
|
||||
// unescape value
|
||||
if (typeof v == 'string') {
|
||||
v = v
|
||||
?.replace(/\\\{/g, '{')
|
||||
?.replace(/\\\}/g, '}')
|
||||
;
|
||||
}
|
||||
this.scope.letVariable(arg.name, v);
|
||||
}
|
||||
for (const arg of this.providedArgumentList) {
|
||||
let v = arg.value;
|
||||
if (v instanceof SlashCommandClosure) {
|
||||
/**@type {SlashCommandClosure}*/
|
||||
const closure = v;
|
||||
closure.scope.parent = this.scope;
|
||||
if (closure.executeNow) {
|
||||
v = (await closure.execute())?.pipe;
|
||||
} else {
|
||||
v = closure;
|
||||
}
|
||||
} else {
|
||||
v = this.substituteParams(v, this.scope.parent);
|
||||
}
|
||||
// unescape value
|
||||
if (typeof v == 'string') {
|
||||
v = v
|
||||
?.replace(/\\\{/g, '{')
|
||||
?.replace(/\\\}/g, '}')
|
||||
;
|
||||
}
|
||||
this.scope.setVariable(arg.name, v);
|
||||
}
|
||||
|
||||
let done = 0;
|
||||
for (const executor of this.executorList) {
|
||||
this.onProgress?.(done, this.commandCount);
|
||||
if (executor instanceof SlashCommandClosureExecutor) {
|
||||
const closure = this.scope.getVariable(executor.name);
|
||||
if (!closure || !(closure instanceof SlashCommandClosure)) throw new Error(`${executor.name} is not a closure.`);
|
||||
closure.scope.parent = this.scope;
|
||||
closure.providedArgumentList = executor.providedArgumentList;
|
||||
const result = await closure.execute();
|
||||
this.scope.pipe = result.pipe;
|
||||
} else {
|
||||
let args = {
|
||||
_scope: this.scope,
|
||||
_parserFlags: executor.parserFlags,
|
||||
};
|
||||
let value;
|
||||
// substitute named arguments
|
||||
for (const arg of executor.namedArgumentList) {
|
||||
if (arg.value instanceof SlashCommandClosure) {
|
||||
/**@type {SlashCommandClosure}*/
|
||||
const closure = arg.value;
|
||||
closure.scope.parent = this.scope;
|
||||
if (closure.executeNow) {
|
||||
args[arg.name] = (await closure.execute())?.pipe;
|
||||
} else {
|
||||
args[arg.name] = closure;
|
||||
}
|
||||
} else {
|
||||
args[arg.name] = this.substituteParams(arg.value);
|
||||
}
|
||||
// unescape named argument
|
||||
if (typeof args[arg.name] == 'string') {
|
||||
args[arg.name] = args[arg.name]
|
||||
?.replace(/\\\{/g, '{')
|
||||
?.replace(/\\\}/g, '}')
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
// substitute unnamed argument
|
||||
if (executor.unnamedArgumentList.length == 0) {
|
||||
if (executor.injectPipe) {
|
||||
value = this.scope.pipe;
|
||||
}
|
||||
} else {
|
||||
value = [];
|
||||
for (let i = 0; i < executor.unnamedArgumentList.length; i++) {
|
||||
let v = executor.unnamedArgumentList[i].value;
|
||||
if (v instanceof SlashCommandClosure) {
|
||||
/**@type {SlashCommandClosure}*/
|
||||
const closure = v;
|
||||
closure.scope.parent = this.scope;
|
||||
if (closure.executeNow) {
|
||||
v = (await closure.execute())?.pipe;
|
||||
} else {
|
||||
v = closure;
|
||||
}
|
||||
} else {
|
||||
v = this.substituteParams(v);
|
||||
}
|
||||
value[i] = v;
|
||||
}
|
||||
if (!executor.command.splitUnnamedArgument) {
|
||||
if (value.length == 1) {
|
||||
value = value[0];
|
||||
} else if (!value.find(it=>it instanceof SlashCommandClosure)) {
|
||||
value = value.join(' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
// unescape unnamed argument
|
||||
if (typeof value == 'string') {
|
||||
value = value
|
||||
?.replace(/\\\{/g, '{')
|
||||
?.replace(/\\\}/g, '}')
|
||||
;
|
||||
}
|
||||
|
||||
let abortResult = await this.testAbortController();
|
||||
if (abortResult) {
|
||||
return abortResult;
|
||||
}
|
||||
executor.onProgress = (subDone, subTotal)=>this.onProgress?.(done + subDone, this.commandCount);
|
||||
this.scope.pipe = await executor.command.callback(args, value ?? '');
|
||||
done += executor.commandCount;
|
||||
this.onProgress?.(done, this.commandCount);
|
||||
abortResult = await this.testAbortController();
|
||||
if (abortResult) {
|
||||
return abortResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
/**@type {SlashCommandClosureResult} */
|
||||
const result = Object.assign(new SlashCommandClosureResult(), { pipe: this.scope.pipe });
|
||||
return result;
|
||||
}
|
||||
|
||||
async testPaused() {
|
||||
while (!this.abortController?.signal?.aborted && this.abortController?.signal?.paused) {
|
||||
await delay(200);
|
||||
}
|
||||
}
|
||||
async testAbortController() {
|
||||
await this.testPaused();
|
||||
if (this.abortController?.signal?.aborted) {
|
||||
const result = new SlashCommandClosureResult();
|
||||
result.isAborted = true;
|
||||
result.abortReason = this.abortController.signal.reason.toString();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { SlashCommandNamedArgumentAssignment } from './SlashCommandNamedArgumentAssignment.js';
|
||||
|
||||
export class SlashCommandClosureExecutor {
|
||||
/**@type {String}*/ name = '';
|
||||
// @ts-ignore
|
||||
/**@type {SlashCommandNamedArgumentAssignment[]}*/ providedArgumentList = [];
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
export class SlashCommandClosureResult {
|
||||
/**@type {boolean}*/ interrupt = false;
|
||||
/**@type {string}*/ pipe;
|
||||
/**@type {boolean}*/ isAborted = false;
|
||||
/**@type {string}*/ abortReason;
|
||||
/**@type {boolean}*/ isError = false;
|
||||
/**@type {string}*/ errorMessage;
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import { SlashCommand } from './SlashCommand.js';
|
||||
import { AutoCompleteOption } from '../autocomplete/AutoCompleteOption.js';
|
||||
|
||||
export class SlashCommandCommandAutoCompleteOption extends AutoCompleteOption {
|
||||
/**@type {SlashCommand}*/ command;
|
||||
|
||||
|
||||
get value() {
|
||||
return this.command;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @param {SlashCommand} command
|
||||
* @param {string} name
|
||||
*/
|
||||
constructor(command, name) {
|
||||
super(name);
|
||||
this.command = command;
|
||||
}
|
||||
|
||||
|
||||
renderItem() {
|
||||
let li;
|
||||
li = this.command.renderHelpItem(this.name);
|
||||
li.setAttribute('data-name', this.name);
|
||||
li.setAttribute('data-option-type', 'command');
|
||||
return li;
|
||||
}
|
||||
|
||||
|
||||
renderDetails() {
|
||||
return this.command.renderHelpDetails(this.name);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
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(cmd, enumValue) {
|
||||
super(enumValue.value, '◊');
|
||||
this.cmd = cmd;
|
||||
this.enumValue = enumValue;
|
||||
}
|
||||
|
||||
|
||||
renderItem() {
|
||||
let li;
|
||||
li = this.makeItem(this.name, '◊', true, [], [], null, this.enumValue.description);
|
||||
li.setAttribute('data-name', this.name);
|
||||
li.setAttribute('data-option-type', 'enum');
|
||||
return li;
|
||||
}
|
||||
|
||||
|
||||
renderDetails() {
|
||||
return this.cmd.renderHelpDetails();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
export class SlashCommandEnumValue {
|
||||
/**@type {string}*/ value;
|
||||
/**@type {string}*/ description;
|
||||
|
||||
constructor(value, description = null) {
|
||||
this.value = value;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.value;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
// eslint-disable-next-line no-unused-vars
|
||||
import { SlashCommand } from './SlashCommand.js';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { SlashCommandClosure } from './SlashCommandClosure.js';
|
||||
import { SlashCommandNamedArgumentAssignment } from './SlashCommandNamedArgumentAssignment.js';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { PARSER_FLAG } from './SlashCommandParser.js';
|
||||
import { SlashCommandUnnamedArgumentAssignment } from './SlashCommandUnnamedArgumentAssignment.js';
|
||||
|
||||
export class SlashCommandExecutor {
|
||||
/**@type {Boolean}*/ injectPipe = true;
|
||||
/**@type {Number}*/ start;
|
||||
/**@type {Number}*/ end;
|
||||
/**@type {Number}*/ startNamedArgs;
|
||||
/**@type {Number}*/ endNamedArgs;
|
||||
/**@type {Number}*/ startUnnamedArgs;
|
||||
/**@type {Number}*/ endUnnamedArgs;
|
||||
/**@type {String}*/ name = '';
|
||||
/**@type {SlashCommand}*/ command;
|
||||
// @ts-ignore
|
||||
/**@type {SlashCommandNamedArgumentAssignment[]}*/ namedArgumentList = [];
|
||||
/**@type {SlashCommandUnnamedArgumentAssignment[]}*/ unnamedArgumentList = [];
|
||||
/**@type {Object<PARSER_FLAG,boolean>} */ parserFlags;
|
||||
|
||||
get commandCount() {
|
||||
return 1
|
||||
+ this.namedArgumentList.filter(it=>it.value instanceof SlashCommandClosure).map(it=>/**@type {SlashCommandClosure}*/(it.value).commandCount).reduce((cur, sum)=>cur + sum, 0)
|
||||
+ this.unnamedArgumentList.filter(it=>it.value instanceof SlashCommandClosure).map(it=>/**@type {SlashCommandClosure}*/(it.value).commandCount).reduce((cur, sum)=>cur + sum, 0)
|
||||
;
|
||||
}
|
||||
|
||||
set onProgress(value) {
|
||||
const closures = /**@type {SlashCommandClosure[]}*/([
|
||||
...this.namedArgumentList.filter(it=>it.value instanceof SlashCommandClosure).map(it=>it.value),
|
||||
...this.unnamedArgumentList.filter(it=>it.value instanceof SlashCommandClosure).map(it=>it.value),
|
||||
]);
|
||||
for (const closure of closures) {
|
||||
closure.onProgress = value;
|
||||
}
|
||||
}
|
||||
|
||||
constructor(start) {
|
||||
this.start = start;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import { SlashCommandClosure } from './SlashCommandClosure.js';
|
||||
|
||||
export class SlashCommandNamedArgumentAssignment {
|
||||
/**@type {number}*/ start;
|
||||
/**@type {number}*/ end;
|
||||
/**@type {string}*/ name;
|
||||
/**@type {string|SlashCommandClosure}*/ value;
|
||||
|
||||
|
||||
constructor() {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
import { AutoCompleteOption } from '../autocomplete/AutoCompleteOption.js';
|
||||
import { SlashCommand } from './SlashCommand.js';
|
||||
import { SlashCommandNamedArgument } from './SlashCommandArgument.js';
|
||||
import { SlashCommandNamedArgumentAssignment } from './SlashCommandNamedArgumentAssignment.js';
|
||||
|
||||
export class SlashCommandNamedArgumentAutoCompleteOption extends AutoCompleteOption {
|
||||
/**@type {SlashCommandNamedArgument}*/ arg;
|
||||
/**@type {SlashCommand}*/ cmd;
|
||||
|
||||
/**
|
||||
* @param {SlashCommandNamedArgument} arg
|
||||
*/
|
||||
constructor(arg, cmd) {
|
||||
super(`${arg.name}=`);
|
||||
this.arg = arg;
|
||||
this.cmd = cmd;
|
||||
}
|
||||
|
||||
|
||||
renderItem() {
|
||||
let li;
|
||||
li = this.makeItem(this.name, '⌗', true, [], [], null, `${this.arg.isRequired ? '' : '(optional) '}${this.arg.description ?? ''}`);
|
||||
li.setAttribute('data-name', this.name);
|
||||
li.setAttribute('data-option-type', 'namedArgument');
|
||||
return li;
|
||||
}
|
||||
|
||||
|
||||
renderDetails() {
|
||||
return this.cmd.renderHelpDetails();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,957 @@
|
|||
import { power_user } from '../power-user.js';
|
||||
import { isTrueBoolean, uuidv4 } from '../utils.js';
|
||||
import { SlashCommand } from './SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument } from './SlashCommandArgument.js';
|
||||
import { SlashCommandClosure } from './SlashCommandClosure.js';
|
||||
import { SlashCommandExecutor } from './SlashCommandExecutor.js';
|
||||
import { SlashCommandParserError } from './SlashCommandParserError.js';
|
||||
import { AutoCompleteNameResult } from '../autocomplete/AutoCompleteNameResult.js';
|
||||
import { SlashCommandQuickReplyAutoCompleteOption } from './SlashCommandQuickReplyAutoCompleteOption.js';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { SlashCommandScope } from './SlashCommandScope.js';
|
||||
import { SlashCommandVariableAutoCompleteOption } from './SlashCommandVariableAutoCompleteOption.js';
|
||||
import { SlashCommandNamedArgumentAssignment } from './SlashCommandNamedArgumentAssignment.js';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { SlashCommandAbortController } from './SlashCommandAbortController.js';
|
||||
import { SlashCommandAutoCompleteNameResult } from './SlashCommandAutoCompleteNameResult.js';
|
||||
import { SlashCommandUnnamedArgumentAssignment } from './SlashCommandUnnamedArgumentAssignment.js';
|
||||
import { SlashCommandEnumValue } from './SlashCommandEnumValue.js';
|
||||
import { MacroAutoCompleteOption } from '../autocomplete/MacroAutoCompleteOption.js';
|
||||
|
||||
/**@readonly*/
|
||||
/**@enum {Number}*/
|
||||
export const PARSER_FLAG = {
|
||||
'STRICT_ESCAPING': 1,
|
||||
'REPLACE_GETVAR': 2,
|
||||
};
|
||||
|
||||
export class SlashCommandParser {
|
||||
// @ts-ignore
|
||||
/**@type {Object.<string, SlashCommand>}*/ static commands = {};
|
||||
|
||||
/**
|
||||
* @deprecated Use SlashCommandParser.addCommandObject() instead.
|
||||
* @param {string} command Command name
|
||||
* @param {(namedArguments:Object.<string,string|SlashCommandClosure>, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|void|Promise<string|SlashCommandClosure|void>} callback The function to execute when the command is called
|
||||
* @param {string[]} aliases List of alternative command names
|
||||
* @param {string} helpString Help text shown in autocomplete and command browser
|
||||
*/
|
||||
static addCommand(command, callback, aliases, helpString = '') {
|
||||
this.addCommandObject(SlashCommand.fromProps({
|
||||
name: command,
|
||||
callback,
|
||||
aliases,
|
||||
helpString,
|
||||
}));
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {SlashCommand} command
|
||||
*/
|
||||
static addCommandObject(command) {
|
||||
const reserved = ['/', '#', ':', 'parser-flag'];
|
||||
for (const start of reserved) {
|
||||
if (command.name.toLowerCase().startsWith(start) || (command.aliases ?? []).find(a=>a.toLowerCase().startsWith(start))) {
|
||||
throw new Error(`Illegal Name. Slash command name cannot begin with "${start}".`);
|
||||
}
|
||||
}
|
||||
this.addCommandObjectUnsafe(command);
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {SlashCommand} command
|
||||
*/
|
||||
static addCommandObjectUnsafe(command) {
|
||||
if ([command.name, ...command.aliases].some(x => Object.hasOwn(this.commands, x))) {
|
||||
console.trace('WARN: Duplicate slash command registered!', [command.name, ...command.aliases]);
|
||||
}
|
||||
|
||||
this.commands[command.name] = command;
|
||||
|
||||
if (Array.isArray(command.aliases)) {
|
||||
command.aliases.forEach((alias) => {
|
||||
this.commands[alias] = command;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
get commands() {
|
||||
return SlashCommandParser.commands;
|
||||
}
|
||||
// @ts-ignore
|
||||
/**@type {Object.<string, string>}*/ helpStrings = {};
|
||||
/**@type {boolean}*/ verifyCommandNames = true;
|
||||
/**@type {string}*/ text;
|
||||
/**@type {number}*/ index;
|
||||
/**@type {SlashCommandAbortController}*/ abortController;
|
||||
/**@type {SlashCommandScope}*/ scope;
|
||||
/**@type {SlashCommandClosure}*/ closure;
|
||||
|
||||
/**@type {Object.<PARSER_FLAG,boolean>}*/ flags = {};
|
||||
|
||||
/**@type {boolean}*/ jumpedEscapeSequence = false;
|
||||
|
||||
/**@type {{start:number, end:number}[]}*/ closureIndex;
|
||||
/**@type {{start:number, end:number, name:string}[]}*/ macroIndex;
|
||||
/**@type {SlashCommandExecutor[]}*/ commandIndex;
|
||||
/**@type {SlashCommandScope[]}*/ scopeIndex;
|
||||
|
||||
get userIndex() { return this.index; }
|
||||
|
||||
get ahead() {
|
||||
return this.text.slice(this.index + 1);
|
||||
}
|
||||
get behind() {
|
||||
return this.text.slice(0, this.index);
|
||||
}
|
||||
get char() {
|
||||
return this.text[this.index];
|
||||
}
|
||||
get endOfText() {
|
||||
return this.index >= this.text.length || /^\s+$/.test(this.ahead);
|
||||
}
|
||||
|
||||
|
||||
constructor() {
|
||||
//TODO should not be re-registered from every instance
|
||||
// add dummy commands for help strings / autocomplete
|
||||
if (!Object.keys(this.commands).includes('parser-flag')) {
|
||||
const help = {};
|
||||
help[PARSER_FLAG.REPLACE_GETVAR] = 'Replace all {{getvar::}} and {{getglobalvar::}} macros with scoped variables to avoid double macro substitution.';
|
||||
help[PARSER_FLAG.STRICT_ESCAPING] = 'Allows to escape all delimiters with backslash, and allows escaping of backslashes.';
|
||||
SlashCommandParser.addCommandObjectUnsafe(SlashCommand.fromProps({ name: 'parser-flag',
|
||||
unnamedArgumentList: [
|
||||
SlashCommandArgument.fromProps({
|
||||
description: 'The parser flag to modify.',
|
||||
typeList: [ARGUMENT_TYPE.STRING],
|
||||
isRequired: true,
|
||||
enumList: Object.keys(PARSER_FLAG).map(flag=>new SlashCommandEnumValue(flag, help[PARSER_FLAG[flag]])),
|
||||
}),
|
||||
SlashCommandArgument.fromProps({
|
||||
description: 'The state of the parser flag to set.',
|
||||
typeList: [ARGUMENT_TYPE.BOOLEAN],
|
||||
defaultValue: 'on',
|
||||
enumList: ['on', 'off'],
|
||||
}),
|
||||
],
|
||||
splitUnnamedArgument: true,
|
||||
helpString: 'Set a parser flag.',
|
||||
}));
|
||||
}
|
||||
if (!Object.keys(this.commands).includes('/')) {
|
||||
SlashCommandParser.addCommandObjectUnsafe(SlashCommand.fromProps({ name: '/',
|
||||
aliases: ['#'],
|
||||
unnamedArgumentList: [
|
||||
SlashCommandArgument.fromProps({
|
||||
description: 'commentary',
|
||||
typeList: [ARGUMENT_TYPE.STRING],
|
||||
}),
|
||||
],
|
||||
helpString: 'Write a comment.',
|
||||
}));
|
||||
}
|
||||
|
||||
//TODO should not be re-registered from every instance
|
||||
this.registerLanguage();
|
||||
}
|
||||
registerLanguage() {
|
||||
// NUMBER mode is copied from highlightjs's own implementation for JavaScript
|
||||
// https://tc39.es/ecma262/#sec-literals-numeric-literals
|
||||
const decimalDigits = '[0-9](_?[0-9])*';
|
||||
const frac = `\\.(${decimalDigits})`;
|
||||
// DecimalIntegerLiteral, including Annex B NonOctalDecimalIntegerLiteral
|
||||
// https://tc39.es/ecma262/#sec-additional-syntax-numeric-literals
|
||||
const decimalInteger = '0|[1-9](_?[0-9])*|0[0-7]*[89][0-9]*';
|
||||
const NUMBER = {
|
||||
className: 'number',
|
||||
variants: [
|
||||
// DecimalLiteral
|
||||
{ begin: `(\\b(${decimalInteger})((${frac})|\\.)?|(${frac}))` +
|
||||
`[eE][+-]?(${decimalDigits})\\b` },
|
||||
{ begin: `\\b(${decimalInteger})\\b((${frac})\\b|\\.)?|(${frac})\\b` },
|
||||
|
||||
// DecimalBigIntegerLiteral
|
||||
{ begin: '\\b(0|[1-9](_?[0-9])*)n\\b' },
|
||||
|
||||
// NonDecimalIntegerLiteral
|
||||
{ begin: '\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*n?\\b' },
|
||||
{ begin: '\\b0[bB][0-1](_?[0-1])*n?\\b' },
|
||||
{ begin: '\\b0[oO][0-7](_?[0-7])*n?\\b' },
|
||||
|
||||
// LegacyOctalIntegerLiteral (does not include underscore separators)
|
||||
// https://tc39.es/ecma262/#sec-additional-syntax-numeric-literals
|
||||
{ begin: '\\b0[0-7]+n?\\b' },
|
||||
],
|
||||
relevance: 0,
|
||||
};
|
||||
|
||||
function getQuotedRunRegex() {
|
||||
try {
|
||||
return new RegExp('(".+?(?<!\\\\)")|(\\S+?)');
|
||||
} catch {
|
||||
// fallback for browsers that don't support lookbehind
|
||||
return /(".+?")|(\S+?)/;
|
||||
}
|
||||
}
|
||||
|
||||
const COMMENT = {
|
||||
scope: 'comment',
|
||||
begin: /\/[/#]/,
|
||||
end: /\||$|:}/,
|
||||
contains: [],
|
||||
};
|
||||
const ABORT = {
|
||||
scope: 'abort',
|
||||
begin: /\/abort/,
|
||||
end: /\||$|:}/,
|
||||
contains: [],
|
||||
};
|
||||
const LET = {
|
||||
begin: [
|
||||
/\/(let|var)\s+/,
|
||||
],
|
||||
beginScope: {
|
||||
1: 'variable',
|
||||
},
|
||||
end: /\||$|:}/,
|
||||
contains: [],
|
||||
};
|
||||
const SETVAR = {
|
||||
begin: /\/(setvar|setglobalvar)\s+/,
|
||||
beginScope: 'variable',
|
||||
end: /\||$|:}/,
|
||||
excludeEnd: true,
|
||||
contains: [],
|
||||
};
|
||||
const GETVAR = {
|
||||
begin: /\/(getvar|getglobalvar)\s+/,
|
||||
beginScope: 'variable',
|
||||
end: /\||$|:}/,
|
||||
excludeEnd: true,
|
||||
contains: [],
|
||||
};
|
||||
const RUN = {
|
||||
match: [
|
||||
/\/:/,
|
||||
getQuotedRunRegex(),
|
||||
],
|
||||
className: {
|
||||
1: 'variable.language',
|
||||
2: 'title.function.invoke',
|
||||
},
|
||||
contains: [], // defined later
|
||||
};
|
||||
const COMMAND = {
|
||||
scope: 'command',
|
||||
begin: /\/\S+/,
|
||||
beginScope: 'title.function',
|
||||
end: /\||$|(?=:})/,
|
||||
excludeEnd: true,
|
||||
contains: [], // defined later
|
||||
};
|
||||
const CLOSURE = {
|
||||
scope: 'closure',
|
||||
begin: /{:/,
|
||||
end: /:}(\(\))?/,
|
||||
beginScope: 'punctuation',
|
||||
endScope: 'punctuation',
|
||||
contains: [], // defined later
|
||||
};
|
||||
const NAMED_ARG = {
|
||||
scope: 'property',
|
||||
begin: /\w+=/,
|
||||
end: '',
|
||||
};
|
||||
const MACRO = {
|
||||
scope: 'variable',
|
||||
begin: /{{/,
|
||||
end: /}}/,
|
||||
};
|
||||
RUN.contains.push(
|
||||
hljs.BACKSLASH_ESCAPE,
|
||||
NAMED_ARG,
|
||||
hljs.QUOTE_STRING_MODE,
|
||||
NUMBER,
|
||||
MACRO,
|
||||
CLOSURE,
|
||||
);
|
||||
LET.contains.push(
|
||||
hljs.BACKSLASH_ESCAPE,
|
||||
NAMED_ARG,
|
||||
NUMBER,
|
||||
MACRO,
|
||||
CLOSURE,
|
||||
hljs.QUOTE_STRING_MODE,
|
||||
);
|
||||
SETVAR.contains.push(
|
||||
hljs.BACKSLASH_ESCAPE,
|
||||
NAMED_ARG,
|
||||
NUMBER,
|
||||
MACRO,
|
||||
CLOSURE,
|
||||
hljs.QUOTE_STRING_MODE,
|
||||
);
|
||||
GETVAR.contains.push(
|
||||
hljs.BACKSLASH_ESCAPE,
|
||||
NAMED_ARG,
|
||||
hljs.QUOTE_STRING_MODE,
|
||||
NUMBER,
|
||||
MACRO,
|
||||
CLOSURE,
|
||||
);
|
||||
COMMAND.contains.push(
|
||||
hljs.BACKSLASH_ESCAPE,
|
||||
NAMED_ARG,
|
||||
NUMBER,
|
||||
MACRO,
|
||||
CLOSURE,
|
||||
hljs.QUOTE_STRING_MODE,
|
||||
);
|
||||
CLOSURE.contains.push(
|
||||
hljs.BACKSLASH_ESCAPE,
|
||||
COMMENT,
|
||||
ABORT,
|
||||
NAMED_ARG,
|
||||
NUMBER,
|
||||
MACRO,
|
||||
RUN,
|
||||
LET,
|
||||
GETVAR,
|
||||
SETVAR,
|
||||
COMMAND,
|
||||
'self',
|
||||
hljs.QUOTE_STRING_MODE,
|
||||
);
|
||||
hljs.registerLanguage('stscript', ()=>({
|
||||
case_insensitive: false,
|
||||
keywords: ['|'],
|
||||
contains: [
|
||||
hljs.BACKSLASH_ESCAPE,
|
||||
COMMENT,
|
||||
ABORT,
|
||||
RUN,
|
||||
LET,
|
||||
GETVAR,
|
||||
SETVAR,
|
||||
COMMAND,
|
||||
CLOSURE,
|
||||
],
|
||||
}));
|
||||
}
|
||||
|
||||
getHelpString() {
|
||||
return '<div class="slashHelp">Loading...</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} text The text to parse.
|
||||
* @param {*} index Index to check for names (cursor position).
|
||||
*/
|
||||
async getNameAt(text, index) {
|
||||
if (this.text != text) {
|
||||
try {
|
||||
this.parse(text, false);
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
console.warn(e);
|
||||
}
|
||||
}
|
||||
const executor = this.commandIndex
|
||||
.filter(it=>it.start <= index && (it.end >= index || it.end == null))
|
||||
.slice(-1)[0]
|
||||
?? null
|
||||
;
|
||||
|
||||
if (executor) {
|
||||
const childClosure = this.closureIndex
|
||||
.find(it=>it.start <= index && (it.end >= index || it.end == null) && it.start > executor.start)
|
||||
?? null
|
||||
;
|
||||
if (childClosure !== null) return null;
|
||||
const macro = this.macroIndex.findLast(it=>it.start <= index && it.end >= index);
|
||||
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(
|
||||
li.querySelector('tt').textContent.slice(2, -2).replace(/^([^\s:]+[\s:]+).*$/, '$1'),
|
||||
li.querySelector('tt').textContent,
|
||||
(li.querySelector('tt').remove(),li.innerHTML),
|
||||
));
|
||||
const result = new AutoCompleteNameResult(
|
||||
macro.name,
|
||||
macro.start + 2,
|
||||
options,
|
||||
false,
|
||||
()=>`No matching macros for "{{${result.name}}}"`,
|
||||
()=>'No macros found.',
|
||||
);
|
||||
return result;
|
||||
}
|
||||
if (executor.name == ':') {
|
||||
const options = this.scopeIndex[this.commandIndex.indexOf(executor)]
|
||||
?.allVariableNames
|
||||
?.map(it=>new SlashCommandVariableAutoCompleteOption(it))
|
||||
?? []
|
||||
;
|
||||
try {
|
||||
const qrApi = (await import('../extensions/quick-reply/index.js')).quickReplyApi;
|
||||
options.push(...qrApi.listSets()
|
||||
.map(set=>qrApi.listQuickReplies(set).map(qr=>`${set}.${qr}`))
|
||||
.flat()
|
||||
.map(qr=>new SlashCommandQuickReplyAutoCompleteOption(qr)),
|
||||
);
|
||||
} catch { /* empty */ }
|
||||
const result = new AutoCompleteNameResult(
|
||||
executor.unnamedArgumentList[0]?.value.toString(),
|
||||
executor.start,
|
||||
options,
|
||||
true,
|
||||
()=>`No matching variables in scope and no matching Quick Replies for "${result.name}"`,
|
||||
()=>'No variables in scope and no Quick Replies found.',
|
||||
);
|
||||
return result;
|
||||
}
|
||||
const result = new SlashCommandAutoCompleteNameResult(executor, this.commands);
|
||||
return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the index <length> number of characters forward and returns the last character taken.
|
||||
* @param {number} length Number of characters to take.
|
||||
* @param {boolean} keep Whether to add the characters to the kept text.
|
||||
* @returns The last character taken.
|
||||
*/
|
||||
take(length = 1) {
|
||||
this.jumpedEscapeSequence = false;
|
||||
let content = this.char;
|
||||
this.index++;
|
||||
if (length > 1) {
|
||||
content = this.take(length - 1);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
discardWhitespace() {
|
||||
while (/\s/.test(this.char)) {
|
||||
this.take(); // discard whitespace
|
||||
this.jumpedEscapeSequence = false;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Tests if the next characters match a symbol.
|
||||
* Moves the index forward if the next characters are backslashes directly followed by the symbol.
|
||||
* Expects that the current char is taken after testing.
|
||||
* @param {string|RegExp} sequence Sequence of chars or regex character group that is the symbol.
|
||||
* @param {number} offset Offset from the current index (won't move the index if offset != 0).
|
||||
* @returns Whether the next characters are the indicated symbol.
|
||||
*/
|
||||
testSymbol(sequence, offset = 0) {
|
||||
if (!this.flags[PARSER_FLAG.STRICT_ESCAPING]) return this.testSymbolLooseyGoosey(sequence, offset);
|
||||
// /echo abc | /echo def
|
||||
// -> TOAST: abc
|
||||
// -> TOAST: def
|
||||
// /echo abc \| /echo def
|
||||
// -> TOAST: abc | /echo def
|
||||
// /echo abc \\| /echo def
|
||||
// -> TOAST: abc \
|
||||
// -> TOAST: def
|
||||
// /echo abc \\\| /echo def
|
||||
// -> TOAST: abc \| /echo def
|
||||
// /echo abc \\\\| /echo def
|
||||
// -> TOAST: abc \\
|
||||
// -> TOAST: def
|
||||
// /echo title=\:} \{: | /echo title=\{: \:}
|
||||
// -> TOAST: *:}* {:
|
||||
// -> TOAST: *{:* :}
|
||||
const escapeOffset = this.jumpedEscapeSequence ? -1 : 0;
|
||||
const escapes = this.text.slice(this.index + offset + escapeOffset).replace(/^(\\*).*$/s, '$1').length;
|
||||
const test = (sequence instanceof RegExp) ?
|
||||
(text) => new RegExp(`^${sequence.source}`).test(text) :
|
||||
(text) => text.startsWith(sequence)
|
||||
;
|
||||
if (test(this.text.slice(this.index + offset + escapeOffset + escapes))) {
|
||||
// no backslashes before sequence
|
||||
// -> sequence found
|
||||
if (escapes == 0) return true;
|
||||
// uneven number of backslashes before sequence
|
||||
// = the final backslash escapes the sequence
|
||||
// = every preceding pair is one literal backslash
|
||||
// -> move index forward to skip the backslash escaping the first backslash or the symbol
|
||||
// even number of backslashes before sequence
|
||||
// = every pair is one literal backslash
|
||||
// -> move index forward to skip the backslash escaping the first backslash
|
||||
if (!this.jumpedEscapeSequence && offset == 0) {
|
||||
this.index++;
|
||||
this.jumpedEscapeSequence = true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
testSymbolLooseyGoosey(sequence, offset = 0) {
|
||||
const escapeOffset = this.jumpedEscapeSequence ? -1 : 0;
|
||||
const escapes = this.text[this.index + offset + escapeOffset] == '\\' ? 1 : 0;
|
||||
const test = (sequence instanceof RegExp) ?
|
||||
(text) => new RegExp(`^${sequence.source}`).test(text) :
|
||||
(text) => text.startsWith(sequence)
|
||||
;
|
||||
if (test(this.text.slice(this.index + offset + escapeOffset + escapes))) {
|
||||
// no backslashes before sequence
|
||||
// -> sequence found
|
||||
if (escapes == 0) return true;
|
||||
// otherwise
|
||||
// -> sequence found
|
||||
if (!this.jumpedEscapeSequence && offset == 0) {
|
||||
this.index++;
|
||||
this.jumpedEscapeSequence = true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
replaceGetvar(value) {
|
||||
return value.replace(/{{(get(?:global)?var)::([^}]+)}}/gi, (_, cmd, name) => {
|
||||
name = name.trim();
|
||||
// store pipe
|
||||
const pipeName = `_PARSER_${uuidv4()}`;
|
||||
const storePipe = new SlashCommandExecutor(null); {
|
||||
storePipe.command = this.commands['let'];
|
||||
storePipe.name = 'let';
|
||||
const nameAss = new SlashCommandUnnamedArgumentAssignment();
|
||||
nameAss.value = pipeName;
|
||||
const valAss = new SlashCommandUnnamedArgumentAssignment();
|
||||
valAss.value = '{{pipe}}';
|
||||
storePipe.unnamedArgumentList = [nameAss, valAss];
|
||||
this.closure.executorList.push(storePipe);
|
||||
}
|
||||
// getvar / getglobalvar
|
||||
const getvar = new SlashCommandExecutor(null); {
|
||||
getvar.command = this.commands[cmd];
|
||||
getvar.name = 'cmd';
|
||||
const nameAss = new SlashCommandUnnamedArgumentAssignment();
|
||||
nameAss.value = name;
|
||||
getvar.unnamedArgumentList = [nameAss];
|
||||
this.closure.executorList.push(getvar);
|
||||
}
|
||||
// set to temp scoped var
|
||||
const varName = `_PARSER_${uuidv4()}`;
|
||||
const setvar = new SlashCommandExecutor(null); {
|
||||
setvar.command = this.commands['let'];
|
||||
setvar.name = 'let';
|
||||
const nameAss = new SlashCommandUnnamedArgumentAssignment();
|
||||
nameAss.value = varName;
|
||||
const valAss = new SlashCommandUnnamedArgumentAssignment();
|
||||
valAss.value = '{{pipe}}';
|
||||
setvar.unnamedArgumentList = [nameAss, valAss];
|
||||
this.closure.executorList.push(setvar);
|
||||
}
|
||||
// return pipe
|
||||
const returnPipe = new SlashCommandExecutor(null); {
|
||||
returnPipe.command = this.commands['return'];
|
||||
returnPipe.name = 'return';
|
||||
const varAss = new SlashCommandUnnamedArgumentAssignment();
|
||||
varAss.value = `{{var::${pipeName}}}`;
|
||||
returnPipe.unnamedArgumentList = [varAss];
|
||||
this.closure.executorList.push(returnPipe);
|
||||
}
|
||||
return `{{var::${varName}}}`;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
parse(text, verifyCommandNames = true, flags = null, abortController = null) {
|
||||
this.verifyCommandNames = verifyCommandNames;
|
||||
for (const key of Object.keys(PARSER_FLAG)) {
|
||||
this.flags[PARSER_FLAG[key]] = flags?.[PARSER_FLAG[key]] ?? power_user.stscript.parser.flags[PARSER_FLAG[key]] ?? false;
|
||||
}
|
||||
this.abortController = abortController;
|
||||
this.text = text;
|
||||
this.index = 0;
|
||||
this.scope = null;
|
||||
this.closureIndex = [];
|
||||
this.commandIndex = [];
|
||||
this.scopeIndex = [];
|
||||
this.macroIndex = [];
|
||||
const closure = this.parseClosure(true);
|
||||
return closure;
|
||||
}
|
||||
|
||||
testClosure() {
|
||||
return this.testSymbol('{:');
|
||||
}
|
||||
testClosureEnd() {
|
||||
if (!this.scope.parent) {
|
||||
// "root" closure does not have {: and :}
|
||||
if (this.index >= this.text.length) return true;
|
||||
return false;
|
||||
}
|
||||
if (!this.verifyCommandNames) {
|
||||
if (this.index >= this.text.length) return true;
|
||||
} else {
|
||||
if (this.ahead.length < 1) throw new SlashCommandParserError(`Unclosed closure at position ${this.userIndex}`, this.text, this.index);
|
||||
}
|
||||
return this.testSymbol(':}');
|
||||
}
|
||||
parseClosure(isRoot = false) {
|
||||
const closureIndexEntry = { start:this.index + 1, end:null };
|
||||
this.closureIndex.push(closureIndexEntry);
|
||||
let injectPipe = true;
|
||||
if (!isRoot) this.take(2); // discard opening {:
|
||||
let closure = new SlashCommandClosure(this.scope);
|
||||
closure.abortController = this.abortController;
|
||||
this.scope = closure.scope;
|
||||
this.closure = closure;
|
||||
this.discardWhitespace();
|
||||
while (this.testNamedArgument()) {
|
||||
const arg = this.parseNamedArgument();
|
||||
closure.argumentList.push(arg);
|
||||
this.scope.variableNames.push(arg.name);
|
||||
this.discardWhitespace();
|
||||
}
|
||||
while (!this.testClosureEnd()) {
|
||||
if (this.testComment()) {
|
||||
this.parseComment();
|
||||
} else if (this.testParserFlag()) {
|
||||
this.parseParserFlag();
|
||||
} else if (this.testRunShorthand()) {
|
||||
const cmd = this.parseRunShorthand();
|
||||
closure.executorList.push(cmd);
|
||||
injectPipe = true;
|
||||
} else if (this.testCommand()) {
|
||||
const cmd = this.parseCommand();
|
||||
cmd.injectPipe = injectPipe;
|
||||
closure.executorList.push(cmd);
|
||||
injectPipe = true;
|
||||
} else {
|
||||
while (!this.testCommandEnd()) this.take(); // discard plain text and comments
|
||||
}
|
||||
this.discardWhitespace();
|
||||
// first pipe marks end of command
|
||||
if (this.testSymbol('|')) {
|
||||
this.take(); // discard first pipe
|
||||
// second pipe indicates no pipe injection for the next command
|
||||
if (this.testSymbol('|')) {
|
||||
injectPipe = false;
|
||||
this.take(); // discard second pipe
|
||||
}
|
||||
}
|
||||
this.discardWhitespace(); // discard further whitespace
|
||||
}
|
||||
if (!isRoot) this.take(2); // discard closing :}
|
||||
if (this.testSymbol('()')) {
|
||||
this.take(2); // discard ()
|
||||
closure.executeNow = true;
|
||||
}
|
||||
closureIndexEntry.end = this.index - 1;
|
||||
this.discardWhitespace(); // discard trailing whitespace
|
||||
this.scope = closure.scope.parent;
|
||||
return closure;
|
||||
}
|
||||
|
||||
testComment() {
|
||||
return this.testSymbol(/\/[/#]/);
|
||||
}
|
||||
testCommentEnd() {
|
||||
return this.testCommandEnd();
|
||||
}
|
||||
parseComment() {
|
||||
const start = this.index + 1;
|
||||
const cmd = new SlashCommandExecutor(start);
|
||||
cmd.command = this.commands['/'];
|
||||
this.commandIndex.push(cmd);
|
||||
this.scopeIndex.push(this.scope.getCopy());
|
||||
this.take(); // discard "/"
|
||||
cmd.name = this.take(); // set second "/" or "#" as name
|
||||
while (!this.testCommentEnd()) this.take();
|
||||
cmd.end = this.index;
|
||||
}
|
||||
|
||||
testParserFlag() {
|
||||
return this.testSymbol('/parser-flag ');
|
||||
}
|
||||
testParserFlagEnd() {
|
||||
return this.testCommandEnd();
|
||||
}
|
||||
parseParserFlag() {
|
||||
const start = this.index + 1;
|
||||
const cmd = new SlashCommandExecutor(start);
|
||||
cmd.name = 'parser-flag';
|
||||
cmd.unnamedArgumentList = [];
|
||||
cmd.command = this.commands[cmd.name];
|
||||
this.commandIndex.push(cmd);
|
||||
this.scopeIndex.push(this.scope.getCopy());
|
||||
this.take(13); // discard "/parser-flag "
|
||||
cmd.startNamedArgs = -1;
|
||||
cmd.endNamedArgs = -1;
|
||||
cmd.startUnnamedArgs = this.index;
|
||||
cmd.unnamedArgumentList = this.parseUnnamedArgument(true);
|
||||
const [flag, state] = cmd.unnamedArgumentList ?? [null, null];
|
||||
cmd.endUnnamedArgs = this.index;
|
||||
if (Object.keys(PARSER_FLAG).includes(flag.value.toString())) {
|
||||
this.flags[PARSER_FLAG[flag.value.toString()]] = isTrueBoolean(state?.value.toString() ?? 'on');
|
||||
}
|
||||
cmd.end = this.index;
|
||||
}
|
||||
|
||||
testRunShorthand() {
|
||||
return this.testSymbol('/:') && !this.testSymbol(':}', 1);
|
||||
}
|
||||
testRunShorthandEnd() {
|
||||
return this.testCommandEnd();
|
||||
}
|
||||
parseRunShorthand() {
|
||||
const start = this.index + 2;
|
||||
const cmd = new SlashCommandExecutor(start);
|
||||
cmd.name = ':';
|
||||
cmd.unnamedArgumentList = [];
|
||||
cmd.command = this.commands['run'];
|
||||
this.commandIndex.push(cmd);
|
||||
this.scopeIndex.push(this.scope.getCopy());
|
||||
this.take(2); //discard "/:"
|
||||
const assignment = new SlashCommandUnnamedArgumentAssignment();
|
||||
if (this.testQuotedValue()) assignment.value = this.parseQuotedValue();
|
||||
else assignment.value = this.parseValue();
|
||||
cmd.unnamedArgumentList = [assignment];
|
||||
this.discardWhitespace();
|
||||
while (this.testNamedArgument()) {
|
||||
const arg = this.parseNamedArgument();
|
||||
cmd.namedArgumentList.push(arg);
|
||||
this.discardWhitespace();
|
||||
}
|
||||
this.discardWhitespace();
|
||||
// /run shorthand does not take unnamed arguments (the command name practically *is* the unnamed argument)
|
||||
if (this.testRunShorthandEnd()) {
|
||||
cmd.end = this.index;
|
||||
return cmd;
|
||||
} else {
|
||||
console.warn(this.behind, this.char, this.ahead);
|
||||
throw new SlashCommandParserError(`Unexpected end of command at position ${this.userIndex}: "/${cmd.name}"`, this.text, this.index);
|
||||
}
|
||||
}
|
||||
|
||||
testCommand() {
|
||||
return this.testSymbol('/');
|
||||
}
|
||||
testCommandEnd() {
|
||||
return this.testClosureEnd() || this.testSymbol('|');
|
||||
}
|
||||
parseCommand() {
|
||||
const start = this.index + 1;
|
||||
const cmd = new SlashCommandExecutor(start);
|
||||
cmd.parserFlags = Object.assign({}, this.flags);
|
||||
this.commandIndex.push(cmd);
|
||||
this.scopeIndex.push(this.scope.getCopy());
|
||||
this.take(); // discard "/"
|
||||
while (!/\s/.test(this.char) && !this.testCommandEnd()) cmd.name += this.take(); // take chars until whitespace or end
|
||||
this.discardWhitespace();
|
||||
if (this.verifyCommandNames && !this.commands[cmd.name]) throw new SlashCommandParserError(`Unknown command at position ${this.index - cmd.name.length}: "/${cmd.name}"`, this.text, this.index - cmd.name.length);
|
||||
cmd.command = this.commands[cmd.name];
|
||||
cmd.startNamedArgs = this.index;
|
||||
cmd.endNamedArgs = this.index;
|
||||
while (this.testNamedArgument()) {
|
||||
const arg = this.parseNamedArgument();
|
||||
cmd.namedArgumentList.push(arg);
|
||||
cmd.endNamedArgs = this.index;
|
||||
this.discardWhitespace();
|
||||
}
|
||||
this.discardWhitespace();
|
||||
cmd.startUnnamedArgs = this.index;
|
||||
cmd.endUnnamedArgs = this.index;
|
||||
if (this.testUnnamedArgument()) {
|
||||
cmd.unnamedArgumentList = this.parseUnnamedArgument(cmd.command?.unnamedArgumentList?.length && cmd?.command?.splitUnnamedArgument);
|
||||
cmd.endUnnamedArgs = this.index;
|
||||
if (cmd.name == 'let') {
|
||||
const keyArg = cmd.namedArgumentList.find(it=>it.name == 'key');
|
||||
if (keyArg) {
|
||||
this.scope.variableNames.push(keyArg.value.toString());
|
||||
} else if (typeof cmd.unnamedArgumentList[0]?.value == 'string') {
|
||||
this.scope.variableNames.push(cmd.unnamedArgumentList[0].value);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.testCommandEnd()) {
|
||||
cmd.end = this.index;
|
||||
return cmd;
|
||||
} else {
|
||||
console.warn(this.behind, this.char, this.ahead);
|
||||
throw new SlashCommandParserError(`Unexpected end of command at position ${this.userIndex}: "/${cmd.name}"`, this.text, this.index);
|
||||
}
|
||||
}
|
||||
|
||||
testNamedArgument() {
|
||||
return /^(\w+)=/.test(`${this.char}${this.ahead}`);
|
||||
}
|
||||
parseNamedArgument() {
|
||||
let assignment = new SlashCommandNamedArgumentAssignment();
|
||||
assignment.start = this.index;
|
||||
let key = '';
|
||||
while (/\w/.test(this.char)) key += this.take(); // take chars
|
||||
this.take(); // discard "="
|
||||
assignment.name = key;
|
||||
if (this.testClosure()) {
|
||||
assignment.value = this.parseClosure();
|
||||
} else if (this.testQuotedValue()) {
|
||||
assignment.value = this.parseQuotedValue();
|
||||
} else if (this.testListValue()) {
|
||||
assignment.value = this.parseListValue();
|
||||
} else if (this.testValue()) {
|
||||
assignment.value = this.parseValue();
|
||||
}
|
||||
assignment.end = this.index;
|
||||
return assignment;
|
||||
}
|
||||
|
||||
testUnnamedArgument() {
|
||||
return !this.testCommandEnd();
|
||||
}
|
||||
testUnnamedArgumentEnd() {
|
||||
return this.testCommandEnd();
|
||||
}
|
||||
parseUnnamedArgument(split) {
|
||||
/**@type {SlashCommandClosure|String}*/
|
||||
let value = this.jumpedEscapeSequence ? this.take() : ''; // take the first, already tested, char if it is an escaped one
|
||||
let isList = split;
|
||||
let listValues = [];
|
||||
/**@type {SlashCommandUnnamedArgumentAssignment}*/
|
||||
let assignment = new SlashCommandUnnamedArgumentAssignment();
|
||||
assignment.start = this.index;
|
||||
while (!this.testUnnamedArgumentEnd()) {
|
||||
if (this.testClosure()) {
|
||||
isList = true;
|
||||
if (value.length > 0) {
|
||||
assignment.end = assignment.end - (value.length - value.trim().length);
|
||||
this.indexMacros(this.index - value.length, value);
|
||||
assignment.value = value.trim();
|
||||
listValues.push(assignment);
|
||||
assignment = new SlashCommandUnnamedArgumentAssignment();
|
||||
assignment.start = this.index;
|
||||
value = '';
|
||||
}
|
||||
assignment.value = this.parseClosure();
|
||||
assignment.end = this.index;
|
||||
listValues.push(assignment);
|
||||
assignment = new SlashCommandUnnamedArgumentAssignment();
|
||||
assignment.start = this.index;
|
||||
} else if (split) {
|
||||
if (this.testQuotedValue()) {
|
||||
assignment.start = this.index;
|
||||
assignment.value = this.parseQuotedValue();
|
||||
assignment.end = this.index;
|
||||
listValues.push(assignment);
|
||||
assignment = new SlashCommandUnnamedArgumentAssignment();
|
||||
} else if (this.testListValue()) {
|
||||
assignment.start = this.index;
|
||||
assignment.value = this.parseListValue();
|
||||
assignment.end = this.index;
|
||||
listValues.push(assignment);
|
||||
assignment = new SlashCommandUnnamedArgumentAssignment();
|
||||
} else if (this.testValue()) {
|
||||
assignment.start = this.index;
|
||||
assignment.value = this.parseValue();
|
||||
assignment.end = this.index;
|
||||
listValues.push(assignment);
|
||||
assignment = new SlashCommandUnnamedArgumentAssignment();
|
||||
} else {
|
||||
throw new SlashCommandParserError(`Unexpected end of unnamed argument at index ${this.userIndex}.`);
|
||||
}
|
||||
this.discardWhitespace();
|
||||
} else {
|
||||
value += this.take();
|
||||
assignment.end = this.index;
|
||||
}
|
||||
}
|
||||
if (isList && value.trim().length > 0) {
|
||||
assignment.value = value.trim();
|
||||
listValues.push(assignment);
|
||||
}
|
||||
if (isList) {
|
||||
return listValues;
|
||||
}
|
||||
this.indexMacros(this.index - value.length, value);
|
||||
value = value.trim();
|
||||
if (this.flags[PARSER_FLAG.REPLACE_GETVAR]) {
|
||||
value = this.replaceGetvar(value);
|
||||
}
|
||||
assignment.value = value;
|
||||
return [assignment];
|
||||
}
|
||||
|
||||
testQuotedValue() {
|
||||
return this.testSymbol('"');
|
||||
}
|
||||
testQuotedValueEnd() {
|
||||
if (this.endOfText) {
|
||||
if (this.verifyCommandNames) throw new SlashCommandParserError(`Unexpected end of quoted value at position ${this.index}`, this.text, this.index);
|
||||
else return true;
|
||||
}
|
||||
if (!this.verifyCommandNames && this.testClosureEnd()) return true;
|
||||
if (this.verifyCommandNames && !this.flags[PARSER_FLAG.STRICT_ESCAPING] && this.testCommandEnd()) {
|
||||
throw new SlashCommandParserError(`Unexpected end of quoted value at position ${this.index}`, this.text, this.index);
|
||||
}
|
||||
return this.testSymbol('"') || (!this.flags[PARSER_FLAG.STRICT_ESCAPING] && this.testCommandEnd());
|
||||
}
|
||||
parseQuotedValue() {
|
||||
this.take(); // discard opening quote
|
||||
let value = '';
|
||||
while (!this.testQuotedValueEnd()) value += this.take(); // take all chars until closing quote
|
||||
this.take(); // discard closing quote
|
||||
if (this.flags[PARSER_FLAG.REPLACE_GETVAR]) {
|
||||
value = this.replaceGetvar(value);
|
||||
}
|
||||
this.indexMacros(this.index - value.length, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
testListValue() {
|
||||
return this.testSymbol('[');
|
||||
}
|
||||
testListValueEnd() {
|
||||
if (this.endOfText) throw new SlashCommandParserError(`Unexpected end of list value at position ${this.index}`, this.text, this.index);
|
||||
return this.testSymbol(']');
|
||||
}
|
||||
parseListValue() {
|
||||
let value = this.take(); // take the already tested opening bracket
|
||||
while (!this.testListValueEnd()) value += this.take(); // take all chars until closing bracket
|
||||
value += this.take(); // take closing bracket
|
||||
if (this.flags[PARSER_FLAG.REPLACE_GETVAR]) {
|
||||
value = this.replaceGetvar(value);
|
||||
}
|
||||
this.indexMacros(this.index - value.length, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
testValue() {
|
||||
return !this.testSymbol(/\s/);
|
||||
}
|
||||
testValueEnd() {
|
||||
if (this.testSymbol(/\s/)) return true;
|
||||
return this.testCommandEnd();
|
||||
}
|
||||
parseValue() {
|
||||
let value = this.jumpedEscapeSequence ? this.take() : ''; // take the first, already tested, char if it is an escaped one
|
||||
while (!this.testValueEnd()) value += this.take(); // take all chars until value end
|
||||
if (this.flags[PARSER_FLAG.REPLACE_GETVAR]) {
|
||||
value = this.replaceGetvar(value);
|
||||
}
|
||||
this.indexMacros(this.index - value.length, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
indexMacros(offset, text) {
|
||||
const re = /{{(?:((?:(?!}})[^\s:])+[\s:]*)((?:(?!}}).)*)(}}|}$|$))?/s;
|
||||
let remaining = text;
|
||||
let localOffset = 0;
|
||||
while (remaining.length > 0 && re.test(remaining)) {
|
||||
const match = re.exec(remaining);
|
||||
this.macroIndex.push({
|
||||
start: offset + localOffset + match.index,
|
||||
end: offset + localOffset + match.index + (match[0]?.length ?? 0),
|
||||
name: match[1] ?? '',
|
||||
});
|
||||
localOffset += match.index + (match[0]?.length ?? 0);
|
||||
remaining = remaining.slice(match.index + (match[0]?.length ?? 0));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
export class SlashCommandParserError extends Error {
|
||||
/**@type {String}*/ text;
|
||||
/**@type {Number}*/ index;
|
||||
|
||||
get line() {
|
||||
return this.text.slice(0, this.index).replace(/[^\n]/g, '').length;
|
||||
}
|
||||
get column() {
|
||||
return this.text.slice(0, this.index).split('\n').pop().length;
|
||||
}
|
||||
get hint() {
|
||||
let lineOffset = this.line.toString().length;
|
||||
let lineStart = this.index;
|
||||
let start = this.index;
|
||||
let end = this.index;
|
||||
let offset = 0;
|
||||
let lineCount = 0;
|
||||
while (offset < 10000 && lineCount < 3 && start >= 0) {
|
||||
if (this.text[start] == '\n') lineCount++;
|
||||
if (lineCount == 0) lineStart--;
|
||||
offset++;
|
||||
start--;
|
||||
}
|
||||
if (this.text[start + 1] == '\n') start++;
|
||||
offset = 0;
|
||||
while (offset < 10000 && this.text[end] != '\n') {
|
||||
offset++;
|
||||
end++;
|
||||
}
|
||||
let hint = [];
|
||||
let lines = this.text.slice(start + 1, end - 1).split('\n');
|
||||
let lineNum = this.line - lines.length + 1;
|
||||
let tabOffset = 0;
|
||||
for (const line of lines) {
|
||||
const num = `${' '.repeat(lineOffset - lineNum.toString().length)}${lineNum}`;
|
||||
lineNum++;
|
||||
const untabbedLine = line.replace(/\t/g, ' '.repeat(4));
|
||||
tabOffset = untabbedLine.length - line.length;
|
||||
hint.push(`${num}: ${untabbedLine}`);
|
||||
}
|
||||
hint.push(`${' '.repeat(this.index - lineStart + lineOffset + 1 + tabOffset)}^^^^^`);
|
||||
return hint.join('\n');
|
||||
}
|
||||
|
||||
constructor(message, text, index) {
|
||||
super(message);
|
||||
this.text = text;
|
||||
this.index = index;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import { AutoCompleteOption } from '../autocomplete/AutoCompleteOption.js';
|
||||
|
||||
export class SlashCommandQuickReplyAutoCompleteOption extends AutoCompleteOption {
|
||||
/**
|
||||
* @param {string} name
|
||||
*/
|
||||
constructor(name) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
|
||||
renderItem() {
|
||||
let li;
|
||||
li = this.makeItem(this.name, 'QR', true);
|
||||
li.setAttribute('data-name', this.name);
|
||||
li.setAttribute('data-option-type', 'qr');
|
||||
return li;
|
||||
}
|
||||
|
||||
|
||||
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 = 'Quick Reply';
|
||||
frag.append(help);
|
||||
}
|
||||
return frag;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
import { SlashCommandClosure } from './SlashCommandClosure.js';
|
||||
|
||||
export class SlashCommandScope {
|
||||
/**@type {string[]}*/ variableNames = [];
|
||||
get allVariableNames() {
|
||||
const names = [...this.variableNames, ...(this.parent?.allVariableNames ?? [])];
|
||||
return names.filter((it,idx)=>idx == names.indexOf(it));
|
||||
}
|
||||
// @ts-ignore
|
||||
/**@type {object.<string, string|SlashCommandClosure>}*/ variables = {};
|
||||
// @ts-ignore
|
||||
/**@type {object.<string, string|SlashCommandClosure>}*/ macros = {};
|
||||
/**@type {{key:string, value:string|SlashCommandClosure}[]} */
|
||||
get macroList() {
|
||||
return [...Object.keys(this.macros).map(key=>({ key, value:this.macros[key] })), ...(this.parent?.macroList ?? [])];
|
||||
}
|
||||
/**@type {SlashCommandScope}*/ parent;
|
||||
/**@type {string}*/ #pipe;
|
||||
get pipe() {
|
||||
return this.#pipe ?? this.parent?.pipe;
|
||||
}
|
||||
set pipe(value) {
|
||||
this.#pipe = value;
|
||||
}
|
||||
|
||||
|
||||
constructor(parent) {
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
getCopy() {
|
||||
const scope = new SlashCommandScope(this.parent);
|
||||
scope.variableNames = [...this.variableNames];
|
||||
scope.variables = Object.assign({}, this.variables);
|
||||
scope.macros = Object.assign({}, this.macros);
|
||||
scope.#pipe = this.#pipe;
|
||||
return scope;
|
||||
}
|
||||
|
||||
|
||||
setMacro(key, value) {
|
||||
this.macros[key] = value;
|
||||
}
|
||||
|
||||
|
||||
existsVariableInScope(key) {
|
||||
return Object.keys(this.variables).includes(key);
|
||||
}
|
||||
existsVariable(key) {
|
||||
return Object.keys(this.variables).includes(key) || this.parent?.existsVariable(key);
|
||||
}
|
||||
letVariable(key, value = undefined) {
|
||||
if (this.existsVariableInScope(key)) throw new SlashCommandScopeVariableExistsError(`Variable named "${key}" already exists.`);
|
||||
this.variables[key] = value;
|
||||
}
|
||||
setVariable(key, value, index = null) {
|
||||
if (this.existsVariableInScope(key)) {
|
||||
if (index !== null && index !== undefined) {
|
||||
let v = this.variables[key];
|
||||
try {
|
||||
v = JSON.parse(v);
|
||||
const numIndex = Number(index);
|
||||
if (Number.isNaN(numIndex)) {
|
||||
v[index] = value;
|
||||
} else {
|
||||
v[numIndex] = value;
|
||||
}
|
||||
v = JSON.stringify(v);
|
||||
} catch {
|
||||
v[index] = value;
|
||||
}
|
||||
this.variables[key] = v;
|
||||
} else {
|
||||
this.variables[key] = value;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
if (this.parent) {
|
||||
return this.parent.setVariable(key, value, index);
|
||||
}
|
||||
throw new SlashCommandScopeVariableNotFoundError(`No such variable: "${key}"`);
|
||||
}
|
||||
getVariable(key, index = null) {
|
||||
if (this.existsVariableInScope(key)) {
|
||||
if (index !== null && index !== undefined) {
|
||||
let v = this.variables[key];
|
||||
try { v = JSON.parse(v); } catch { /* empty */ }
|
||||
const numIndex = Number(index);
|
||||
if (Number.isNaN(numIndex)) {
|
||||
v = v[index];
|
||||
} else {
|
||||
v = v[numIndex];
|
||||
}
|
||||
if (typeof v == 'object') return JSON.stringify(v);
|
||||
return v;
|
||||
} else {
|
||||
const value = this.variables[key];
|
||||
return (value === '' || isNaN(Number(value))) ? (value || '') : Number(value);
|
||||
}
|
||||
}
|
||||
if (this.parent) {
|
||||
return this.parent.getVariable(key, index);
|
||||
}
|
||||
throw new SlashCommandScopeVariableNotFoundError(`No such variable: "${key}"`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export class SlashCommandScopeVariableExistsError extends Error {}
|
||||
|
||||
|
||||
export class SlashCommandScopeVariableNotFoundError extends Error {}
|
|
@ -0,0 +1,11 @@
|
|||
import { SlashCommandClosure } from './SlashCommandClosure.js';
|
||||
|
||||
export class SlashCommandUnnamedArgumentAssignment {
|
||||
/**@type {number}*/ start;
|
||||
/**@type {number}*/ end;
|
||||
/**@type {string|SlashCommandClosure}*/ value;
|
||||
|
||||
|
||||
constructor() {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import { AutoCompleteOption } from '../autocomplete/AutoCompleteOption.js';
|
||||
|
||||
export class SlashCommandVariableAutoCompleteOption extends AutoCompleteOption {
|
||||
/**
|
||||
* @param {string} name
|
||||
*/
|
||||
constructor(name) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
|
||||
renderItem() {
|
||||
let li;
|
||||
li = this.makeItem(this.name, '[𝑥]', true);
|
||||
li.setAttribute('data-name', this.name);
|
||||
li.setAttribute('data-option-type', 'variable');
|
||||
return li;
|
||||
}
|
||||
|
||||
|
||||
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 = 'scoped variable';
|
||||
frag.append(help);
|
||||
}
|
||||
return frag;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue