mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-03-03 03:17:54 +01:00
Merge branch 'SillyTavern:staging' into staging
This commit is contained in:
commit
baab494ed4
2
.github/readme-zh_cn.md
vendored
2
.github/readme-zh_cn.md
vendored
@ -47,7 +47,7 @@ SillyTavern 本身并无用处,因为它只是一个用户聊天界面。你
|
||||
|
||||
获取支持,或分享喜爱的角色和 Prompt:
|
||||
|
||||
### [加入 Discord 社区](https://discord.gg/RZdyAEUPvj)
|
||||
### [加入 Discord 社区](https://discord.gg/sillytavern)
|
||||
|
||||
***
|
||||
|
||||
|
7
.github/readme.md
vendored
7
.github/readme.md
vendored
@ -51,7 +51,7 @@ Since Tavern is only a user interface, it has tiny hardware requirements, it wil
|
||||
|
||||
Get support, share favorite characters and prompts:
|
||||
|
||||
### [Join](https://discord.gg/RZdyAEUPvj)
|
||||
### [Join](https://discord.gg/sillytavern)
|
||||
|
||||
***
|
||||
|
||||
@ -293,8 +293,9 @@ You can find them archived here:
|
||||
|
||||
## Screenshots
|
||||
|
||||
<img width="400" alt="image" src="https://user-images.githubusercontent.com/18619528/228649245-8061c60f-63dc-488e-9325-f151b7a3ec2d.png">
|
||||
<img width="400" alt="image" src="https://user-images.githubusercontent.com/18619528/228649856-fbdeef05-d727-4d5a-be80-266cbbc6b811.png">
|
||||
<img width="400" alt="image" src="https://github.com/SillyTavern/SillyTavern/assets/61471128/e902c7a2-45a6-4415-97aa-c59c597669c1">
|
||||
<img width="400" alt="image" src="https://github.com/SillyTavern/SillyTavern/assets/61471128/f8a79c47-4fe9-4564-9e4a-bf247ed1c961">
|
||||
|
||||
|
||||
## License and credits
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
pushd %~dp0
|
||||
call npm install --no-audit
|
||||
node server.js
|
||||
node server.js %*
|
||||
pause
|
||||
popd
|
||||
|
@ -12,6 +12,6 @@ if %errorlevel% neq 0 (
|
||||
)
|
||||
)
|
||||
call npm install
|
||||
node server.js
|
||||
node server.js %*
|
||||
pause
|
||||
popd
|
||||
|
@ -7,7 +7,7 @@
|
||||
"source": [
|
||||
"**Links**<br>\n",
|
||||
"Extensions API GitHub: https://github.com/SillyTavern/SillyTavern-extras/<br>\n",
|
||||
"SillyTavern community Discord (support and discussion): https://discord.gg/RZdyAEUPvj"
|
||||
"SillyTavern community Discord (support and discussion): https://discord.gg/sillytavern"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
6
package-lock.json
generated
6
package-lock.json
generated
@ -2413,9 +2413,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
||||
"version": "1.15.4",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
|
||||
"integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
|
@ -42,6 +42,9 @@
|
||||
},
|
||||
"vectra": {
|
||||
"openai": "^4.17.0"
|
||||
},
|
||||
"axios": {
|
||||
"follow-redirects": "^1.15.4"
|
||||
}
|
||||
},
|
||||
"name": "sillytavern",
|
||||
|
@ -107,6 +107,12 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.select2-results .select2-results__option--group {
|
||||
color: var(--SmartThemeBodyColor);
|
||||
background-color: var(--SmartThemeBlurTintColor);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Customize the hovered option list item */
|
||||
.select2-results .select2-results__option--highlighted.select2-results__option--selectable {
|
||||
color: var(--SmartThemeBodyColor);
|
||||
@ -114,12 +120,20 @@
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.select2-results__option.select2-results__option--group::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Customize the option list item */
|
||||
.select2-results__option {
|
||||
padding-left: 30px;
|
||||
/* Add some padding to make room for the checkbox */
|
||||
}
|
||||
|
||||
.select2-results .select2-results__option--group .select2-results__options--nested .select2-results__option {
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
/* Add the custom checkbox */
|
||||
.select2-results__option::before {
|
||||
content: '';
|
||||
|
50
public/img/custom.svg
Normal file
50
public/img/custom.svg
Normal file
@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
version="1.0"
|
||||
width="16.986956"
|
||||
height="22.042189"
|
||||
id="svg11382"
|
||||
sodipodi:docname="custom.svg"
|
||||
viewBox="0 0 679.47822 881.68754"
|
||||
inkscape:version="1.1.2 (b8e25be833, 2022-02-05)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
inkscape:zoom="42.995"
|
||||
inkscape:cx="11.082684"
|
||||
inkscape:cy="8.9545296"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1009"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="g11476"
|
||||
id="namedview669"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0" />
|
||||
<defs
|
||||
id="defs11384" />
|
||||
<g
|
||||
id="layer1"
|
||||
transform="translate(-48.907487,-55.625885)">
|
||||
<g
|
||||
id="g11476">
|
||||
<path
|
||||
d="m 465.24901,686.59683 c -51.1537,0 -102.3074,0 -153.4611,0 -1.31708,-52.18472 2.26923,-108.82047 37.00965,-150.43627 23.21285,-33.61844 63.37794,-47.61571 92.45252,-74.91276 37.85456,-29.39299 66.45422,-78.39712 58.32465,-128.1697 -2.77308,-43.29131 -38.24417,-77.99847 -79.30937,-85.0026 -50.42874,-11.82866 -111.67907,2.3265 -139.55528,50.25117 -16.71781,25.76665 -27.12589,57.26058 -22.82769,88.35614 -58.1841,0.51881 -116.3682,1.03762 -174.552298,1.55643 0.16527,-71.42695 3.86133,-151.28527 55.163188,-206.18091 46.46301,-56.01624 114.22212,-93.030055 186.31391,-98.273555 91.93492,-7.78137 193.46191,-4.40991 270.27577,54.209695 53.26315,37.16622 90.35652,97.5512 95.89082,163.52777 10.37716,64.92902 -13.64221,131.79055 -55.35059,180.60583 -35.39219,39.71396 -82.8918,64.29734 -123.59888,97.30398 -32.73006,25.67093 -48.60796,65.61157 -46.7753,107.16478 z"
|
||||
style="stroke-width:0.905819"
|
||||
id="path672" />
|
||||
<path
|
||||
d="m 483.48936,847.70143 a 94.042557,89.593537 0 1 1 -188.08511,0 94.042557,89.593537 0 1 1 188.08511,0 z"
|
||||
style="stroke-width:1.10768"
|
||||
id="path674" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.3 KiB |
@ -536,6 +536,32 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-newbie-hidden class="range-block" data-source="openrouter">
|
||||
<div class="range-block-title" data-i18n="Min P">
|
||||
Min P
|
||||
</div>
|
||||
<div class="range-block-range-and-counter">
|
||||
<div class="range-block-range">
|
||||
<input type="range" id="min_p_openai" name="volume" min="0" max="1" step="0.001">
|
||||
</div>
|
||||
<div class="range-block-counter">
|
||||
<input type="number" min="0" max="1" step="0.001" data-for="min_p_openai" id="min_p_counter_openai">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-newbie-hidden class="range-block" data-source="openrouter">
|
||||
<div class="range-block-title" data-i18n="Top A">
|
||||
Top A
|
||||
</div>
|
||||
<div class="range-block-range-and-counter">
|
||||
<div class="range-block-range">
|
||||
<input type="range" id="top_a_openai" name="volume" min="0" max="1" step="0.001">
|
||||
</div>
|
||||
<div class="range-block-counter">
|
||||
<input type="number" min="0" max="1" step="0.001" data-for="top_a_openai" id="top_a_counter_openai">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-drawer m-t-1 wide100p">
|
||||
<div class="inline-drawer-toggle inline-drawer-header">
|
||||
<b data-i18n="Quick Edit">Quick Prompts Edit</b>
|
||||
@ -1164,6 +1190,12 @@
|
||||
</div>
|
||||
</div><!-- end of novel settings-->
|
||||
<div id="textgenerationwebui_api-settings">
|
||||
<div data-newbie-hidden class="flex-container justifyCenter">
|
||||
<small class="flex-container alignitemscenter">
|
||||
<div id="samplerResetButton" class="menu_button whitespacenowrap">Neutralize Samplers</div>
|
||||
<div class="fa-solid fa-circle-info opacity50p" title="Sets all samplers to their neutral/disabled state."></div>
|
||||
</small>
|
||||
</div>
|
||||
<div data-newbie-hidden data-tg-type="aphrodite" class="flex-container flexFlowColumn alignitemscenter flexBasis100p flexGrow flexShrink gap0">
|
||||
<small data-i18n="Multiple swipes per generation">Multiple swipes per generation</small>
|
||||
<input type="number" id="n_textgenerationwebui" class="text_pole textAlignCenter" min="1" value="1" />
|
||||
@ -1174,8 +1206,8 @@
|
||||
<span data-i18n="temperature">Temperature</span>
|
||||
<div class="fa-solid fa-circle-info opacity50p" title="Temperature controls the randomness in token selection: - low temperature (<1.0) leads to more predictable text, favoring higher probability tokens. - high temperature (>1.0) increases creativity and diversity in the output by giving lower probability tokens a better chance. Set to 1.0 for the original probabilities."></div>
|
||||
</small>
|
||||
<input class="neo-range-slider" type="range" id="temp_textgenerationwebui" name="volume" min="0.0" max="4.0" step="0.01" x-setting-id="temp">
|
||||
<input class="neo-range-input" type="number" min="0.0" max="4.0" step="0.01" data-for="temp_textgenerationwebui" id="temp_counter_textgenerationwebui">
|
||||
<input class="neo-range-slider" type="range" id="temp_textgenerationwebui" name="volume" min="0.0" max="5.0" step="0.01" x-setting-id="temp">
|
||||
<input class="neo-range-input" type="number" min="0.0" max="5.0" step="0.01" data-for="temp_textgenerationwebui" id="temp_counter_textgenerationwebui">
|
||||
</div>
|
||||
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<small>
|
||||
@ -1203,8 +1235,8 @@
|
||||
<span data-i18n="Min P">Min P</span>
|
||||
<div class="fa-solid fa-circle-info opacity50p" title="Min P sets a base minimum probability. This is scaled according to the top token's probability. E.g If Top token is 80% probability, and Min P is 0.1, only tokens higher than 8% would be considered. Set to 0 to disable."></div>
|
||||
</small>
|
||||
<input class="neo-range-slider" type="range" id="min_p_textgenerationwebui" name="volume" min="0" max="1" step="0.01">
|
||||
<input class="neo-range-input" type="number" min="0" max="1" step="0.05" data-for="min_p_textgenerationwebui" id="min_p_counter_textgenerationwebui">
|
||||
<input class="neo-range-slider" type="range" id="min_p_textgenerationwebui" name="volume" min="0" max="1" step="0.001">
|
||||
<input class="neo-range-input" type="number" min="0" max="1" step="0.001" data-for="min_p_textgenerationwebui" id="min_p_counter_textgenerationwebui">
|
||||
</div>
|
||||
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<small data-i18n="Top A">Top A</small>
|
||||
@ -1216,12 +1248,12 @@
|
||||
<input class="neo-range-slider" type="range" id="tfs_textgenerationwebui" name="volume" min="0" max="1" step="0.01">
|
||||
<input class="neo-range-input" type="number" min="0" max="1" step="0.01" data-for="tfs_textgenerationwebui" id="tfs_counter_textgenerationwebui">
|
||||
</div>
|
||||
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<div data-newbie-hidden data-tg-type="ooba" class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<small data-i18n="Epsilon Cutoff">Epsilon Cutoff</small>
|
||||
<input class="neo-range-slider" type="range" id="epsilon_cutoff_textgenerationwebui" name="volume" min="0" max="9" step="0.01">
|
||||
<input class="neo-range-input" type="number" min="0" max="9" step="0.01" data-for="epsilon_cutoff_textgenerationwebui" id="epsilon_cutoff_counter_textgenerationwebui">
|
||||
</div>
|
||||
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<div data-newbie-hidden data-tg-type="ooba" class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<small data-i18n="Eta Cutoff">Eta Cutoff</small>
|
||||
<input class="neo-range-slider" type="range" id="eta_cutoff_textgenerationwebui" name="volume" min="0" max="20" step="0.01">
|
||||
<input class="neo-range-input" type="number" min="0" max="20" step="0.01" data-for="eta_cutoff_textgenerationwebui" id="eta_cutoff_counter_textgenerationwebui">
|
||||
@ -1231,12 +1263,12 @@
|
||||
<input class="neo-range-slider" type="range" id="rep_pen_textgenerationwebui" name="volume" min="1" max="3" step="0.01">
|
||||
<input class="neo-range-input" type="number" min="1" max="3" step="0.01" data-for="rep_pen_textgenerationwebui" id="rep_pen_counter_textgenerationwebui">
|
||||
</div>
|
||||
<div data-forAphro=False class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<div data-forAphro="False" class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<small data-i18n="rep.pen range">Repetition Penalty Range</small>
|
||||
<input class="neo-range-slider" type="range" id="rep_pen_range_textgenerationwebui" name="volume" min="-1" max="8192" step="1">
|
||||
<input class="neo-range-input" type="number" min="-1" max="8192" step="1" data-for="rep_pen_range_textgenerationwebui" id="rep_pen_range_counter_textgenerationwebui">
|
||||
</div>
|
||||
<div data-forAphro=False data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<div data-forAphro="False" data-tg-type="ooba" data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<small data-i18n="Encoder Rep. Pen.">Encoder Penalty</small>
|
||||
<input class="neo-range-slider" type="range" id="encoder_rep_pen_textgenerationwebui" name="volume" min="0.8" max="1.5" step="0.01" />
|
||||
<input class="neo-range-input" type="number" min="0.8" max="1.5" step="0.01" data-for="encoder_rep_pen_textgenerationwebui" id="encoder_rep_pen_counter_textgenerationwebui">
|
||||
@ -1251,12 +1283,12 @@
|
||||
<input class="neo-range-slider" type="range" id="presence_pen_textgenerationwebui" name="volume" min="-2" max="2" step="0.01" />
|
||||
<input class="neo-range-input" type="number" min="-2" max="2" step="0.01" data-for="presence_pen_textgenerationwebui" id="presence_pen_counter_textgenerationwebui">
|
||||
</div>
|
||||
<div data-forAphro=False data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<div data-forAphro="False" data-tg-type="ooba" data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<small data-i18n="No Repeat Ngram Size">No Repeat Ngram Size</small>
|
||||
<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 class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<div data-newbie-hidden data-tg-type="ooba" class="alignitemscenter flex-container flexFlowColumn flexBasis48p 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">
|
||||
@ -1283,6 +1315,30 @@
|
||||
<input class="neo-range-input" type="number" min="0" max="5" step="1" data-for="prompt_log_probs_aphrodite" id="prompt_log_probs_aphrodite_counter_textgenerationwebui">
|
||||
</div>
|
||||
-->
|
||||
<div data-newbie-hidden name="dynaTempBlock" class="wide100p">
|
||||
<h4 class="wide100p textAlignCenter" data-i18n="DynaTemp">
|
||||
<div class="flex-container alignitemscenter" style="justify-content: center;">
|
||||
<div class="checkbox_label" for="dynatemp_textgenerationwebui">
|
||||
<input type="checkbox" id="dynatemp_textgenerationwebui" />
|
||||
<small data-i18n="dynatemp"></small>
|
||||
</div>
|
||||
<span style="text-align: center;">Dynamic Temperature</span>
|
||||
<div class="fa-solid fa-circle-info opacity50p" title="Scales Temperature dynamically per token (based on the variation of probabilities.)"></div>
|
||||
</div>
|
||||
</h4>
|
||||
<div class="flex-container flexFlowRow alignitemscenter gap10px flexShrink">
|
||||
<div class="alignitemscenter flex-container marginBot5 flexFlowColumn flexGrow flexShrink gap0">
|
||||
<small data-i18n="Minimum Temp">Minimum Temp</small>
|
||||
<input class="neo-range-slider" type="range" id="min_temp_textgenerationwebui" name="volume" min="0" max="5" step="0.01" />
|
||||
<input class="neo-range-input" type="number" min="0" max="5" step="0.01" data-for="min_temp_textgenerationwebui" id="min_temp_counter_textgenerationwebui">
|
||||
</div>
|
||||
<div class="alignitemscenter flex-container marginBot5 flexFlowColumn flexGrow flexShrink gap0">
|
||||
<small data-i18n="Maximum Temp">Maximum Temp</small>
|
||||
<input class="neo-range-slider" type="range" id="max_temp_textgenerationwebui" name="volume" min="0" max="5" step="0.01" />
|
||||
<input class="neo-range-input" type="number" min="0" max="5" step="0.01" data-for="max_temp_textgenerationwebui" id="max_temp_counter_textgenerationwebui">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-newbie-hidden name="miroStatBlock" class="wide100p">
|
||||
<h4 class="wide100p textAlignCenter" data-i18n="Mirostat (mode=1 is only for llama.cpp)">Mirostat
|
||||
<div class=" fa-solid fa-circle-info opacity50p " title="Mode=1 is only for llama.cpp More helpful tips coming soon."></div>
|
||||
@ -1305,7 +1361,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-newbie-hidden name="beamSearchBlock" class="wide100p">
|
||||
<div data-newbie-hidden data-tg-type="ooba" name="beamSearchBlock" class="wide100p">
|
||||
<h4 class="wide100p textAlignCenter" span data-i18n="Beam search">Beam Search
|
||||
<div class=" fa-solid fa-circle-info opacity50p " title="Helpful tip coming soon."></div>
|
||||
</h4>
|
||||
@ -1328,7 +1384,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-forAphro=False data-newbie-hidden name="contrastiveSearchBlock" class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<div data-forAphro="False" data-tg-type="ooba" data-newbie-hidden name="contrastiveSearchBlock" class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
|
||||
<h4 class="textAlignCenter" data-i18n="Contrastive search">Contrast Search
|
||||
<div class=" fa-solid fa-circle-info opacity50p " title="Helpful tip coming soon."></div>
|
||||
</h4>
|
||||
@ -1340,17 +1396,17 @@
|
||||
</div>
|
||||
<div data-newbie-hidden name="checkboxes" class="flex-container flexBasis48p justifyCenter flexGrow flexShrink ">
|
||||
<div class="flex-container flexFlowColumn marginTop5">
|
||||
<label data-forAphro=False class="checkbox_label flexGrow flexShrink" for="do_sample_textgenerationwebui">
|
||||
<label data-forAphro="False" data-tg-type="ooba" class="checkbox_label flexGrow flexShrink" for="do_sample_textgenerationwebui">
|
||||
<input type="checkbox" id="do_sample_textgenerationwebui" />
|
||||
<small data-i18n="Do Sample">Do Sample</small>
|
||||
</label>
|
||||
<label data-forAphro=False class="checkbox_label flexGrow flexShrink" for="add_bos_token_textgenerationwebui">
|
||||
<label data-forAphro="False" data-tg-type="ooba, tabby" class="checkbox_label flexGrow flexShrink" for="add_bos_token_textgenerationwebui">
|
||||
<input type="checkbox" id="add_bos_token_textgenerationwebui" />
|
||||
<small data-i18n="Add BOS Token">Add BOS Token
|
||||
<div class="fa-solid fa-circle-info opacity50p " data-i18n="Add the bos_token to the beginning of prompts. Disabling this can make the replies more creative." title="Add the bos_token to the beginning of prompts. Disabling this can make the replies more creative."></div>
|
||||
</small>
|
||||
</label>
|
||||
<label data-forAphro=False class="checkbox_label flexGrow flexShrink" for="ban_eos_token_textgenerationwebui">
|
||||
<label data-forAphro="False" class="checkbox_label flexGrow flexShrink" for="ban_eos_token_textgenerationwebui">
|
||||
<input type="checkbox" id="ban_eos_token_textgenerationwebui" />
|
||||
<small data-i18n="Ban EOS Token">Ban EOS Token
|
||||
<div class="fa-solid fa-circle-info opacity50p " data-i18n="Ban the eos_token. This forces the model to never end the generation prematurely" title="Ban the eos_token. This forces the model to never end the generation prematurely."></div>
|
||||
@ -1364,11 +1420,11 @@
|
||||
</small>
|
||||
</label>
|
||||
|
||||
<label class="checkbox_label flexGrow flexShrink" for="skip_special_tokens_textgenerationwebui">
|
||||
<label class="checkbox_label flexGrow flexShrink" for="skip_special_tokens_textgenerationwebui">
|
||||
<input type="checkbox" id="skip_special_tokens_textgenerationwebui" />
|
||||
<small data-i18n="Skip Special Tokens">Skip Special Tokens</small>
|
||||
</label>
|
||||
<label data-forAphro=False class="checkbox_label flexGrow flexShrink" for="temperature_last_textgenerationwebui">
|
||||
<label data-forAphro="False" data-tg-type="ooba, aphrodite, tabby" class="checkbox_label flexGrow flexShrink" for="temperature_last_textgenerationwebui">
|
||||
<input type="checkbox" id="temperature_last_textgenerationwebui" />
|
||||
<small data-i18n="Temperature Last">Temperature Last
|
||||
<div class="fa-solid fa-circle-info opacity50p " data-i18n="Use the temperature sampler last." title="Use the temperature sampler last."></div>
|
||||
@ -1379,10 +1435,9 @@
|
||||
<input type="checkbox" id="spaces_between_special_tokens_aphrodite_textgenerationwebui" />
|
||||
<small data-i18n="Spaces Between Special Tokens">Spaces Between Special Tokens</small>
|
||||
</label>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div data-forAphro=False data-newbie-hidden class="flex-container flexFlowColumn alignitemscenter flexBasis48p flexGrow flexShrink gap0">
|
||||
<div data-forAphro="False" data-newbie-hidden class="flex-container flexFlowColumn alignitemscenter flexBasis48p flexGrow flexShrink gap0">
|
||||
<small data-i18n="Seed" class="textAlignCenter">Seed</small>
|
||||
<input type="number" id="seed_textgenerationwebui" class="text_pole textAlignCenter" min="-1" value="-1" maxlength="100" />
|
||||
</div>
|
||||
@ -1411,8 +1466,8 @@
|
||||
<div class="logit_bias_list"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-newbie-hidden data-forAphro=False class="wide100p">
|
||||
<hr class="width100p">
|
||||
<div data-newbie-hidden data-forAphro="False" class="wide100p">
|
||||
<hr class="width100p">
|
||||
<h4 data-i18n="CFG" class="textAlignCenter">CFG
|
||||
<div class="margin5 fa-solid fa-circle-info opacity50p " title="Helpful tip coming soon."></div>
|
||||
</h4>
|
||||
@ -1433,7 +1488,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-newbie-hidden data-forAphro=False id="grammar_block_ooba" class="wide100p">
|
||||
<div data-newbie-hidden data-forAphro="False" id="grammar_block_ooba" class="wide100p">
|
||||
<hr class="wide100p">
|
||||
<h4 class="wide100p textAlignCenter" data-i18n="GBNF Grammar">GBNF Grammar
|
||||
<a href="https://github.com/ggerganov/llama.cpp/blob/master/grammars/README.md" target="_blank">
|
||||
@ -1666,11 +1721,11 @@
|
||||
<div class="flex-container flexFlowColumn">
|
||||
<div id="main-API-selector-block">
|
||||
<select id="main_api">
|
||||
<option value="kobold"><span data-i18n="KoboldAI">KoboldAI Classic</span></option>
|
||||
<option value="koboldhorde"><span data-i18n="KoboldAI Horde">KoboldAI Horde</span></option>
|
||||
<option value="novel"><span data-i18n="NovelAI">NovelAI</span></option>
|
||||
<option value="textgenerationwebui"><span data-i18n="Text Completion">Text Completion</span></option>
|
||||
<option value="openai"><span data-i18n="Chat Completion">Chat Completion</span></option>
|
||||
<option value="novel"><span data-i18n="NovelAI">NovelAI</span></option>
|
||||
<option value="koboldhorde"><span data-i18n="KoboldAI Horde">KoboldAI Horde</span></option>
|
||||
<option value="kobold"><span data-i18n="KoboldAI">KoboldAI Classic</span></option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="kobold_horde" style="position: relative;"> <!-- shows the kobold settings -->
|
||||
@ -1740,6 +1795,9 @@
|
||||
<h4 data-i18n="API url">API url</h4>
|
||||
<small data-i18n="Example: http://127.0.0.1:5000/api ">Example: http://127.0.0.1:5000/api </small>
|
||||
<input id="api_url_text" name="api_url" class="text_pole" placeholder="http://127.0.0.1:5000/api" maxlength="500" value="" autocomplete="off" data-server-history="kobold">
|
||||
<div id="koboldcpp_hint" class="neutral_warning displayNone">
|
||||
We have a dedicated KoboldCpp support under Text Completion ⇒ KoboldCpp.
|
||||
</div>
|
||||
<div class="flex-container">
|
||||
<div id="api_button" class="api_button menu_button" type="submit" data-i18n="Connect" data-server-connect="kobold">Connect</div>
|
||||
<div class="api_loading menu_button" data-i18n="Cancel">Cancel</div>
|
||||
@ -1863,6 +1921,7 @@
|
||||
<small data-i18n="Example: http://127.0.0.1:5000 ">Example: http://127.0.0.1:5000</small>
|
||||
<input id="textgenerationwebui_api_url_text" name="textgenerationwebui_api_url" class="text_pole wide100p" maxlength="500" value="" autocomplete="off" data-server-history="ooba_blocking">
|
||||
</div>
|
||||
<input id="custom_model_textgenerationwebui" class="text_pole wide100p" maxlength="500" placeholder="Custom model (optional)" type="text">
|
||||
</div>
|
||||
<div data-tg-type="aphrodite">
|
||||
<div class="flex-container flexFlowColumn">
|
||||
@ -1910,7 +1969,8 @@
|
||||
</div>
|
||||
<div class="flex1">
|
||||
<h4>
|
||||
<span data-i18n="Ollama Model">Ollama Model</h4>
|
||||
<span data-i18n="Ollama Model">Ollama Model
|
||||
</h4>
|
||||
</h4>
|
||||
<select id="ollama_model">
|
||||
<option>
|
||||
@ -1964,6 +2024,10 @@
|
||||
<input type="checkbox" id="legacy_api_textgenerationwebui" />
|
||||
<span data-i18n="Legacy API (pre-OAI, no streaming)">Legacy API (pre-OAI, no streaming)</span>
|
||||
</label>
|
||||
<label data-tg-type="ooba" class="checkbox_label margin-bot-10px" for="bypass_status_check_textgenerationwebui">
|
||||
<input type="checkbox" id="bypass_status_check_textgenerationwebui" />
|
||||
<span data-i18n="Bypass status check">Bypass status check</span>
|
||||
</label>
|
||||
</form>
|
||||
<div class="online_status">
|
||||
<div class="online_status_indicator"></div>
|
||||
@ -2313,10 +2377,10 @@
|
||||
<div class="flex-container">
|
||||
<input id="custom_model_id" class="text_pole wide100p" maxlength="500" value="" autocomplete="off" placeholder="Example: gpt-3.5-turbo">
|
||||
</div>
|
||||
<h4 data-i18n="Available Models">Available Models</h4>
|
||||
<div class="flex-container">
|
||||
<select id="model_custom_select" class="text_pole"></select>
|
||||
</div>
|
||||
<h4 data-i18n="Available Models">Available Models</h4>
|
||||
<div class="flex-container">
|
||||
<select id="model_custom_select" class="text_pole"></select>
|
||||
</div>
|
||||
</form>
|
||||
<div class="flex-container flex">
|
||||
<div id="api_button_openai" class="api_button menu_button menu_button_icon" type="submit" data-i18n="Connect">Connect</div>
|
||||
@ -2793,10 +2857,10 @@
|
||||
</div>
|
||||
<div class="range-block-range-and-counter">
|
||||
<div class="range-block-range paddingLeftRight5">
|
||||
<input type="range" id="world_info_depth" name="volume" min="0" max="100" step="1">
|
||||
<input class="neo-range-slider" type="range" id="world_info_depth" name="volume" min="0" max="100" step="1">
|
||||
</div>
|
||||
<div class="range-block-counter margin0">
|
||||
<input type="number" data-for="world_info_depth" id="world_info_depth_counter">
|
||||
<input class="neo-range-input" type="number" min="0" max="100" step="1" data-for="world_info_depth" id="world_info_depth_counter">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -2806,10 +2870,10 @@
|
||||
</div>
|
||||
<div class="range-block-range-and-counter ">
|
||||
<div class="range-block-range paddingLeftRight5">
|
||||
<input type="range" id="world_info_budget" name="volume" min="1" max="100" step="1">
|
||||
<input class="neo-range-slider" type="range" id="world_info_budget" name="volume" min="1" max="100" step="1">
|
||||
</div>
|
||||
<div class="range-block-counter margin0">
|
||||
<input type="number" min="1" max="100" step="1" data-for="world_info_budget" id="world_info_budget_counter">
|
||||
<input class="neo-range-input" type="number" min="1" max="100" step="1" data-for="world_info_budget" id="world_info_budget_counter">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -2819,10 +2883,10 @@
|
||||
</div>
|
||||
<div class="range-block-range-and-counter ">
|
||||
<div class="range-block-range paddingLeftRight5">
|
||||
<input type="range" id="world_info_budget_cap" name="volume" min="0" max="8192" step="1">
|
||||
<input class="neo-range-slider" type="range" id="world_info_budget_cap" name="volume" min="0" max="8192" step="1">
|
||||
</div>
|
||||
<div class="range-block-counter margin0">
|
||||
<input type="number" min="0" max="8192" step="1" data-for="world_info_budget_cap" id="world_info_budget_cap_counter">
|
||||
<input class="neo-range-input" type="number" min="0" max="8192" step="1" data-for="world_info_budget_cap" id="world_info_budget_cap_counter">
|
||||
</div>
|
||||
</div>
|
||||
<div class="budget_cap_note">
|
||||
@ -2835,10 +2899,10 @@
|
||||
</div>
|
||||
<div class="range-block-range-and-counter">
|
||||
<div class="range-block-range paddingLeftRight5">
|
||||
<input type="range" id="world_info_min_activations" name="volume" min="0" max="100" step="1">
|
||||
<input class="neo-range-slider" type="range" id="world_info_min_activations" name="volume" min="0" max="100" step="1">
|
||||
</div>
|
||||
<div class="range-block-counter margin0">
|
||||
<input type="number" data-for="world_info_min_activations" id="world_info_min_activations_counter">
|
||||
<input class="neo-range-input" type="number" min="0" max="100" step="1" data-for="world_info_min_activations" id="world_info_min_activations_counter">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -2848,10 +2912,10 @@
|
||||
</div>
|
||||
<div class="range-block-range-and-counter">
|
||||
<div class="range-block-range paddingLeftRight5">
|
||||
<input type="range" id="world_info_min_activations_depth_max" name="volume" min="0" max="100" step="1">
|
||||
<input class="neo-range-slider" type="range" id="world_info_min_activations_depth_max" name="volume" min="0" max="100" step="1">
|
||||
</div>
|
||||
<div class="range-block-counter margin0">
|
||||
<input type="number" data-for="world_info_min_activations_depth_max" id="world_info_min_activations_depth_max_counter">
|
||||
<input class="neo-range-input" type="number" min="0" max="100" step="1" data-for="world_info_min_activations_depth_max" id="world_info_min_activations_depth_max_counter">
|
||||
</div>
|
||||
</div>
|
||||
<div class="budget_cap_note">
|
||||
@ -2908,6 +2972,7 @@
|
||||
<div id="world_backfill_memos" class="menu_button fa-solid fa-notes-medical" title="Fill empty Memo/Titles with Keywords" data-i18n="[title]Fill empty Memo/Titles with Keywords"></div>
|
||||
<div id="world_import_button" class="menu_button fa-solid fa-file-import" title="Import World Info" data-i18n="[title]Import World Info"></div>
|
||||
<div id="world_popup_export" class="menu_button fa-solid fa-file-export" title="Export World Info" data-i18n="[title]Export World Info"></div>
|
||||
<div id="world_duplicate" class="menu_button fa-solid fa-paste" title="Duplicate World Info" data-i18n="[title]Duplicate World Info"></div>
|
||||
<div id="world_popup_delete" class="menu_button fa-solid fa-trash-can redWarningBG" title="Delete World Info" data-i18n="[title]Delete World Info"></div>
|
||||
<input type="search" class="text_pole textarea_compact" data-i18n="[placeholder]Search..." id="world_info_search" placeholder="Search...">
|
||||
<select id="world_info_sort_order" class="margin0">
|
||||
@ -4330,7 +4395,13 @@
|
||||
<label class="checkbox flex-container alignitemscenter flexNoGap">
|
||||
<input type="checkbox" name="exclude_recursion" />
|
||||
<span data-i18n="Exclude from recursion">
|
||||
Non-recursable
|
||||
Non-recursable (this entry will not be activated by another)
|
||||
</span>
|
||||
</label>
|
||||
<label class="checkbox flex-container alignitemscenter flexNoGap">
|
||||
<input type="checkbox" name="prevent_recursion" />
|
||||
<span data-i18n="Prevent further recursion (this entry will not activate others)">
|
||||
Prevent further recursion (this entry will not activate others)
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
@ -4354,7 +4425,7 @@
|
||||
<label for="characterFilter" class="">
|
||||
<small data-i18n="Filter to Character(s)">Filter to Character(s)</small>
|
||||
</label>
|
||||
<label class="checkbox flex-container alignitemscenter">
|
||||
<label class="checkbox_label flexNoGap">
|
||||
<input type="checkbox" name="character_exclusion" />
|
||||
<span data-i18n="Character Exclusion">
|
||||
<small>Character Exclusion</small>
|
||||
@ -4433,7 +4504,7 @@
|
||||
<div id="logit_bias_template" class="template_element">
|
||||
<div class="logit_bias_form">
|
||||
<input class="logit_bias_text text_pole" data-i18n="[placeholder]Type here..." placeholder="type here..." />
|
||||
<input class="logit_bias_value text_pole" type="number" min="-2" value="0" max="2" step="0.01" />
|
||||
<input class="logit_bias_value text_pole" type="number" min="-100" value="0" max="100" step="0.01" />
|
||||
<i class="menu_button fa-solid fa-xmark logit_bias_remove"></i>
|
||||
</div>
|
||||
</div>
|
||||
@ -4700,10 +4771,10 @@
|
||||
<h3><span data-i18n="Alternate Greetings">Alternate Greetings</span></h3>
|
||||
<div title="Add" class="menu_button fa-solid fa-plus add_alternate_greeting" data-i18n="[title]Add"></div>
|
||||
</div>
|
||||
<div class="justifyLeft" data-i18n="Alternate Greetings Subtitle">
|
||||
<small class="justifyLeft" data-i18n="Alternate Greetings Subtitle">
|
||||
These will be displayed as swipes on the first message when starting a new chat.
|
||||
Group members can select one of them to initiate the conversation.
|
||||
</div>
|
||||
</small>
|
||||
<hr>
|
||||
<div class="alternate_greetings_list flexFlowColumn flex-container wide100p">
|
||||
<strong class="alternate_grettings_hint margin-bot-10px" data-i18n="Alternate Greetings Hint">
|
||||
@ -4718,7 +4789,7 @@
|
||||
<strong>Alternate Greeting #<span class="greeting_index"></span></strong>
|
||||
<div class="menu_button fa-solid fa-trash-alt delete_alternate_greeting"></div>
|
||||
</div>
|
||||
<textarea name="alternate_greetings" data-i18n="[placeholder](This will be the first message from the character that starts every chat)" placeholder="(This will be the first message from the character that starts every chat)" class="text_pole textarea_compact alternate_greeting_text" maxlength="50000" value="" autocomplete="off" rows="12"></textarea>
|
||||
<textarea name="alternate_greetings" data-i18n="[placeholder](This will be the first message from the character that starts every chat)" placeholder="(This will be the first message from the character that starts every chat)" class="text_pole textarea_compact alternate_greeting_text" maxlength="50000" value="" autocomplete="off" rows="16"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<!-- chat and input bar -->
|
||||
@ -5177,4 +5248,4 @@
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
363
public/script.js
363
public/script.js
@ -16,7 +16,6 @@ import {
|
||||
generateTextGenWithStreaming,
|
||||
getTextGenGenerationData,
|
||||
textgen_types,
|
||||
textgenerationwebui_banned_in_macros,
|
||||
getTextGenServer,
|
||||
validateTextGenUrl,
|
||||
} from './scripts/textgen-settings.js';
|
||||
@ -134,7 +133,6 @@ import {
|
||||
download,
|
||||
isDataURL,
|
||||
getCharaFilename,
|
||||
isDigitsOnly,
|
||||
PAGINATION_TEMPLATE,
|
||||
waitUntilCondition,
|
||||
escapeRegex,
|
||||
@ -180,7 +178,6 @@ import {
|
||||
getInstructStoppingSequences,
|
||||
autoSelectInstructPreset,
|
||||
formatInstructModeSystemPrompt,
|
||||
replaceInstructMacros,
|
||||
} from './scripts/instruct-mode.js';
|
||||
import { applyLocale, initLocales } from './scripts/i18n.js';
|
||||
import { getFriendlyTokenizerName, getTokenCount, getTokenizerModel, initTokenizers, saveTokenCache } from './scripts/tokenizers.js';
|
||||
@ -190,8 +187,8 @@ import { hideLoader, showLoader } from './scripts/loader.js';
|
||||
import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay.js';
|
||||
import { loadMancerModels, loadOllamaModels, loadTogetherAIModels } from './scripts/textgen-models.js';
|
||||
import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags } from './scripts/chats.js';
|
||||
import { replaceVariableMacros } from './scripts/variables.js';
|
||||
import { initPresetManager } from './scripts/preset-manager.js';
|
||||
import { evaluateMacros } from './scripts/macros.js';
|
||||
|
||||
//exporting functions and vars for mods
|
||||
export {
|
||||
@ -536,13 +533,22 @@ $(document).ajaxError(function myErrorHandler(_, xhr) {
|
||||
}
|
||||
});
|
||||
|
||||
function getUrlSync(url, cache = true) {
|
||||
return $.ajax({
|
||||
type: 'GET',
|
||||
url: url,
|
||||
cache: cache,
|
||||
async: false,
|
||||
}).responseText;
|
||||
/**
|
||||
* Loads a URL content using XMLHttpRequest synchronously.
|
||||
* @param {string} url URL to load synchronously
|
||||
* @returns {string} Response text
|
||||
*/
|
||||
function getUrlSync(url) {
|
||||
console.debug('Loading URL synchronously', url);
|
||||
const request = new XMLHttpRequest();
|
||||
request.open('GET', url, false); // `false` makes the request synchronous
|
||||
request.send();
|
||||
|
||||
if (request.status >= 200 && request.status < 300) {
|
||||
return request.responseText;
|
||||
}
|
||||
|
||||
throw new Error(`Error loading ${url}: ${request.status} ${request.statusText}`);
|
||||
}
|
||||
|
||||
const templateCache = new Map();
|
||||
@ -954,6 +960,11 @@ async function getStatusTextgen() {
|
||||
return resultCheckStatus();
|
||||
}
|
||||
|
||||
if (textgen_settings.type == OOBA && textgen_settings.bypass_status_check) {
|
||||
online_status = 'Status check bypassed';
|
||||
return resultCheckStatus();
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
@ -1484,7 +1495,7 @@ export function sendTextareaMessage() {
|
||||
// message was sent from a character (not the user or the system).
|
||||
const textareaText = String($('#send_textarea').val());
|
||||
if (power_user.continue_on_send &&
|
||||
!textareaText &&
|
||||
!textareaText &&
|
||||
!selected_group &&
|
||||
chat.length &&
|
||||
!chat[chat.length - 1]['is_user'] &&
|
||||
@ -2049,88 +2060,6 @@ function scrollChatToBottom() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID of the last message in the chat.
|
||||
* @returns {string} The ID of the last message in the chat.
|
||||
*/
|
||||
function getLastMessageId() {
|
||||
const index = chat?.length - 1;
|
||||
|
||||
if (!isNaN(index) && index >= 0) {
|
||||
return String(index);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID of the first message included in the context.
|
||||
* @returns {string} The ID of the first message in the context.
|
||||
*/
|
||||
function getFirstIncludedMessageId() {
|
||||
const index = document.querySelector('.lastInContext')?.getAttribute('mesid');
|
||||
|
||||
if (!isNaN(index) && index >= 0) {
|
||||
return String(index);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last message in the chat.
|
||||
* @returns {string} The last message in the chat.
|
||||
*/
|
||||
function getLastMessage() {
|
||||
const index = chat?.length - 1;
|
||||
|
||||
if (!isNaN(index) && index >= 0) {
|
||||
return chat[index].mes;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID of the last swipe.
|
||||
* @returns {string} The 1-based ID of the last swipe
|
||||
*/
|
||||
function getLastSwipeId() {
|
||||
const index = chat?.length - 1;
|
||||
|
||||
if (!isNaN(index) && index >= 0) {
|
||||
const swipes = chat[index].swipes;
|
||||
|
||||
if (!Array.isArray(swipes) || swipes.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return String(swipes.length);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID of the current swipe.
|
||||
* @returns {string} The 1-based ID of the current swipe.
|
||||
*/
|
||||
function getCurrentSwipeId() {
|
||||
const index = chat?.length - 1;
|
||||
|
||||
if (!isNaN(index) && index >= 0) {
|
||||
const swipeId = chat[index].swipe_id;
|
||||
|
||||
if (swipeId === undefined || isNaN(swipeId)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return String(swipeId + 1);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Substitutes {{macro}} parameters in a string.
|
||||
* @param {string} content - The string to substitute parameters in.
|
||||
@ -2141,187 +2070,9 @@ function getCurrentSwipeId() {
|
||||
* @returns {string} The string with substituted parameters.
|
||||
*/
|
||||
function substituteParams(content, _name1, _name2, _original, _group, _replaceCharacterCard = true) {
|
||||
_name1 = _name1 ?? name1;
|
||||
_name2 = _name2 ?? name2;
|
||||
_group = _group ?? name2;
|
||||
|
||||
if (!content) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Replace {{original}} with the original message
|
||||
// Note: only replace the first instance of {{original}}
|
||||
// This will hopefully prevent the abuse
|
||||
if (typeof _original === 'string') {
|
||||
content = content.replace(/{{original}}/i, _original);
|
||||
}
|
||||
content = diceRollReplace(content);
|
||||
content = replaceInstructMacros(content);
|
||||
content = replaceVariableMacros(content);
|
||||
content = content.replace(/{{newline}}/gi, '\n');
|
||||
content = content.replace(/{{input}}/gi, String($('#send_textarea').val()));
|
||||
|
||||
if (_replaceCharacterCard) {
|
||||
const fields = getCharacterCardFields();
|
||||
content = content.replace(/{{charPrompt}}/gi, fields.system || '');
|
||||
content = content.replace(/{{charJailbreak}}/gi, fields.jailbreak || '');
|
||||
content = content.replace(/{{description}}/gi, fields.description || '');
|
||||
content = content.replace(/{{personality}}/gi, fields.personality || '');
|
||||
content = content.replace(/{{scenario}}/gi, fields.scenario || '');
|
||||
content = content.replace(/{{persona}}/gi, fields.persona || '');
|
||||
content = content.replace(/{{mesExamples}}/gi, fields.mesExamples || '');
|
||||
}
|
||||
|
||||
content = content.replace(/{{maxPrompt}}/gi, () => String(getMaxContextSize()));
|
||||
content = content.replace(/{{user}}/gi, _name1);
|
||||
content = content.replace(/{{char}}/gi, _name2);
|
||||
content = content.replace(/{{charIfNotGroup}}/gi, _group);
|
||||
content = content.replace(/{{group}}/gi, _group);
|
||||
content = content.replace(/{{lastMessage}}/gi, getLastMessage());
|
||||
content = content.replace(/{{lastMessageId}}/gi, getLastMessageId());
|
||||
content = content.replace(/{{firstIncludedMessageId}}/gi, getFirstIncludedMessageId());
|
||||
content = content.replace(/{{lastSwipeId}}/gi, getLastSwipeId());
|
||||
content = content.replace(/{{currentSwipeId}}/gi, getCurrentSwipeId());
|
||||
|
||||
content = content.replace(/<USER>/gi, _name1);
|
||||
content = content.replace(/<BOT>/gi, _name2);
|
||||
content = content.replace(/<CHARIFNOTGROUP>/gi, _group);
|
||||
content = content.replace(/<GROUP>/gi, _group);
|
||||
|
||||
content = content.replace(/\{\{\/\/([\s\S]*?)\}\}/gm, '');
|
||||
|
||||
content = content.replace(/{{time}}/gi, moment().format('LT'));
|
||||
content = content.replace(/{{date}}/gi, moment().format('LL'));
|
||||
content = content.replace(/{{weekday}}/gi, moment().format('dddd'));
|
||||
content = content.replace(/{{isotime}}/gi, moment().format('HH:mm'));
|
||||
content = content.replace(/{{isodate}}/gi, moment().format('YYYY-MM-DD'));
|
||||
|
||||
content = content.replace(/{{datetimeformat +([^}]*)}}/gi, (_, format) => {
|
||||
const formattedTime = moment().format(format);
|
||||
return formattedTime;
|
||||
});
|
||||
content = content.replace(/{{idle_duration}}/gi, () => getTimeSinceLastMessage());
|
||||
content = content.replace(/{{time_UTC([-+]\d+)}}/gi, (_, offset) => {
|
||||
const utcOffset = parseInt(offset, 10);
|
||||
const utcTime = moment().utc().utcOffset(utcOffset).format('LT');
|
||||
return utcTime;
|
||||
});
|
||||
content = bannedWordsReplace(content);
|
||||
content = randomReplace(content);
|
||||
return content;
|
||||
return evaluateMacros(content, _name1 ?? name1, _name2 ?? name2, _original, _group ?? name2, _replaceCharacterCard);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces banned words in macros with an empty string.
|
||||
* Adds them to textgenerationwebui ban list.
|
||||
* @param {string} inText Text to replace banned words in
|
||||
* @returns {string} Text without the "banned" macro
|
||||
*/
|
||||
function bannedWordsReplace(inText) {
|
||||
if (!inText) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const banPattern = /{{banned "(.*)"}}/gi;
|
||||
|
||||
if (main_api == 'textgenerationwebui') {
|
||||
const bans = inText.matchAll(banPattern);
|
||||
if (bans) {
|
||||
for (const banCase of bans) {
|
||||
console.log('Found banned words in macros: ' + banCase[1]);
|
||||
textgenerationwebui_banned_in_macros.push(banCase[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inText = inText.replaceAll(banPattern, '');
|
||||
return inText;
|
||||
}
|
||||
|
||||
function getTimeSinceLastMessage() {
|
||||
const now = moment();
|
||||
|
||||
if (Array.isArray(chat) && chat.length > 0) {
|
||||
let lastMessage;
|
||||
let takeNext = false;
|
||||
|
||||
for (let i = chat.length - 1; i >= 0; i--) {
|
||||
const message = chat[i];
|
||||
|
||||
if (message.is_system) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (message.is_user && takeNext) {
|
||||
lastMessage = message;
|
||||
break;
|
||||
}
|
||||
|
||||
takeNext = true;
|
||||
}
|
||||
|
||||
if (lastMessage?.send_date) {
|
||||
const lastMessageDate = timestampToMoment(lastMessage.send_date);
|
||||
const duration = moment.duration(now.diff(lastMessageDate));
|
||||
return duration.humanize();
|
||||
}
|
||||
}
|
||||
|
||||
return 'just now';
|
||||
}
|
||||
|
||||
function randomReplace(input, emptyListPlaceholder = '') {
|
||||
const randomPatternNew = /{{random\s?::\s?([^}]+)}}/gi;
|
||||
const randomPatternOld = /{{random\s?:\s?([^}]+)}}/gi;
|
||||
|
||||
if (randomPatternNew.test(input)) {
|
||||
return input.replace(randomPatternNew, (match, listString) => {
|
||||
//split on double colons instead of commas to allow for commas inside random items
|
||||
const list = listString.split('::').filter(item => item.length > 0);
|
||||
if (list.length === 0) {
|
||||
return emptyListPlaceholder;
|
||||
}
|
||||
var rng = new Math.seedrandom('added entropy.', { entropy: true });
|
||||
const randomIndex = Math.floor(rng() * list.length);
|
||||
//trim() at the end to allow for empty random values
|
||||
return list[randomIndex].trim();
|
||||
});
|
||||
} else if (randomPatternOld.test(input)) {
|
||||
return input.replace(randomPatternOld, (match, listString) => {
|
||||
const list = listString.split(',').map(item => item.trim()).filter(item => item.length > 0);
|
||||
if (list.length === 0) {
|
||||
return emptyListPlaceholder;
|
||||
}
|
||||
var rng = new Math.seedrandom('added entropy.', { entropy: true });
|
||||
const randomIndex = Math.floor(rng() * list.length);
|
||||
return list[randomIndex];
|
||||
});
|
||||
} else {
|
||||
return input;
|
||||
}
|
||||
}
|
||||
|
||||
function diceRollReplace(input, invalidRollPlaceholder = '') {
|
||||
const rollPattern = /{{roll[ : ]([^}]+)}}/gi;
|
||||
|
||||
return input.replace(rollPattern, (match, matchValue) => {
|
||||
let formula = matchValue.trim();
|
||||
|
||||
if (isDigitsOnly(formula)) {
|
||||
formula = `1d${formula}`;
|
||||
}
|
||||
|
||||
const isValid = droll.validate(formula);
|
||||
|
||||
if (!isValid) {
|
||||
console.debug(`Invalid roll formula: ${formula}`);
|
||||
return invalidRollPlaceholder;
|
||||
}
|
||||
|
||||
const result = droll.roll(formula);
|
||||
return new String(result.total);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets stopping sequences for the prompt.
|
||||
@ -2369,11 +2120,21 @@ function getStoppingStrings(isImpersonate, isContinue) {
|
||||
* @param {boolean} quietToLoud Whether the message should be sent in a foreground (loud) or background (quiet) mode
|
||||
* @param {boolean} skipWIAN whether to skip addition of World Info and Author's Note into the prompt
|
||||
* @param {string} quietImage Image to use for the quiet prompt
|
||||
* @param {string} quietName Name to use for the quiet prompt (defaults to "System:")
|
||||
* @returns
|
||||
*/
|
||||
export async function generateQuietPrompt(quiet_prompt, quietToLoud, skipWIAN, quietImage = null) {
|
||||
export async function generateQuietPrompt(quiet_prompt, quietToLoud, skipWIAN, quietImage = null, quietName = null) {
|
||||
console.log('got into genQuietPrompt');
|
||||
const generateFinished = await Generate('quiet', { quiet_prompt, quietToLoud, skipWIAN: skipWIAN, force_name2: true, quietImage: quietImage });
|
||||
/** @type {GenerateOptions} */
|
||||
const options = {
|
||||
quiet_prompt,
|
||||
quietToLoud,
|
||||
skipWIAN: skipWIAN,
|
||||
force_name2: true,
|
||||
quietImage: quietImage,
|
||||
quietName: quietName,
|
||||
};
|
||||
const generateFinished = await Generate('quiet', options);
|
||||
return generateFinished;
|
||||
}
|
||||
|
||||
@ -2564,7 +2325,7 @@ 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}}
|
||||
*/
|
||||
function getCharacterCardFields() {
|
||||
export function getCharacterCardFields() {
|
||||
const result = { system: '', mesExamples: '', description: '', personality: '', persona: '', scenario: '', jailbreak: '' };
|
||||
const character = characters[this_chid];
|
||||
|
||||
@ -2947,8 +2708,15 @@ export async function generateRaw(prompt, api, instructOverride) {
|
||||
return message;
|
||||
}
|
||||
|
||||
// Returns a promise that resolves when the text is done generating.
|
||||
async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, maxLoops } = {}, dryRun = false) {
|
||||
/**
|
||||
* Runs a generation using the current chat context.
|
||||
* @param {string} type Generation type
|
||||
* @param {GenerateOptions} options Generation options
|
||||
* @param {boolean} dryRun Whether to actually generate a message or just assemble the prompt
|
||||
* @returns {Promise<any>} Returns a promise that resolves when the text is done generating.
|
||||
* @typedef {{automatic_trigger?: boolean, force_name2?: boolean, quiet_prompt?: string, quietToLoud?: boolean, skipWIAN?: boolean, force_chid?: number, signal?: AbortSignal, quietImage?: string, maxLoops?: number, quietName?: string }} GenerateOptions
|
||||
*/
|
||||
async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, maxLoops, quietName } = {}, dryRun = false) {
|
||||
console.log('Generate entered');
|
||||
eventSource.emit(event_types.GENERATION_STARTED, type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, maxLoops }, dryRun);
|
||||
setGenerationProgress(0);
|
||||
@ -3146,13 +2914,24 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu
|
||||
if (mesExamples.replace(/<START>/gi, '').trim().length === 0) {
|
||||
mesExamples = '';
|
||||
}
|
||||
const mesExamplesRaw = mesExamples;
|
||||
if (mesExamples && isInstruct) {
|
||||
mesExamples = formatInstructModeExamples(mesExamples, name1, name2);
|
||||
}
|
||||
|
||||
const exampleSeparator = power_user.context.example_separator ? `${substituteParams(power_user.context.example_separator)}\n` : '';
|
||||
const blockHeading = main_api === 'openai' ? '<START>\n' : exampleSeparator;
|
||||
let mesExamplesArray = mesExamples.split(/<START>/gi).slice(1).map(block => `${blockHeading}${block.trim()}\n`);
|
||||
/**
|
||||
* Adds a block heading to the examples string.
|
||||
* @param {string} examplesStr
|
||||
* @returns {string[]} Examples array with block heading
|
||||
*/
|
||||
function addBlockHeading(examplesStr) {
|
||||
const exampleSeparator = power_user.context.example_separator ? `${substituteParams(power_user.context.example_separator)}\n` : '';
|
||||
const blockHeading = main_api === 'openai' ? '<START>\n' : exampleSeparator;
|
||||
return examplesStr.split(/<START>/gi).slice(1).map(block => `${blockHeading}${block.trim()}\n`);
|
||||
}
|
||||
|
||||
let mesExamplesArray = addBlockHeading(mesExamples);
|
||||
let mesExamplesRawArray = addBlockHeading(mesExamplesRaw);
|
||||
|
||||
// First message in fresh 1-on-1 chat reacts to user/character settings changes
|
||||
if (chat.length) {
|
||||
@ -3294,6 +3073,7 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu
|
||||
loreBefore: worldInfoBefore,
|
||||
loreAfter: worldInfoAfter,
|
||||
mesExamples: mesExamplesArray.join(''),
|
||||
mesExamplesRaw: mesExamplesRawArray.join(''),
|
||||
};
|
||||
|
||||
const storyString = renderStoryString(storyStringParams);
|
||||
@ -3468,7 +3248,7 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu
|
||||
// need a detection for what the quiet prompt is being asked for...
|
||||
|
||||
// Bail out early?
|
||||
if (quietToLoud !== true) {
|
||||
if (!isInstruct && !quietToLoud) {
|
||||
return lastMesString;
|
||||
}
|
||||
}
|
||||
@ -3476,7 +3256,7 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu
|
||||
|
||||
// Get instruct mode line
|
||||
if (isInstruct && !isContinue) {
|
||||
const name = isImpersonate ? name1 : name2;
|
||||
const name = (quiet_prompt && !quietToLoud) ? (quietName ?? 'System') : (isImpersonate ? name1 : name2);
|
||||
lastMesString += formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2);
|
||||
}
|
||||
|
||||
@ -3684,9 +3464,13 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu
|
||||
scenario,
|
||||
char: name2,
|
||||
user: name1,
|
||||
worldInfoBefore,
|
||||
worldInfoAfter,
|
||||
beforeScenarioAnchor,
|
||||
afterScenarioAnchor,
|
||||
storyString,
|
||||
mesExmString,
|
||||
mesSendString,
|
||||
finalMesSend,
|
||||
generatedPromptCache,
|
||||
main: system,
|
||||
@ -4173,7 +3957,7 @@ export async function sendMessageAsUser(messageText, messageBias, insertAt = nul
|
||||
}
|
||||
}
|
||||
|
||||
function getMaxContextSize() {
|
||||
export function getMaxContextSize() {
|
||||
let this_max_context = 1487;
|
||||
if (main_api == 'kobold' || main_api == 'koboldhorde' || main_api == 'textgenerationwebui') {
|
||||
this_max_context = (max_context - amount_gen);
|
||||
@ -5808,8 +5592,6 @@ async function saveSettings(type) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(background_settings);
|
||||
|
||||
//console.log('Entering settings with name1 = '+name1);
|
||||
|
||||
return jQuery.ajax({
|
||||
@ -6968,7 +6750,7 @@ function openAlternateGreetings() {
|
||||
});
|
||||
|
||||
updateAlternateGreetingsHintVisibility(template);
|
||||
callPopup(template, 'alternate_greeting');
|
||||
callPopup(template, 'alternate_greeting', '', { wide: true, large: true });
|
||||
}
|
||||
|
||||
function addAlternateGreeting(template, greeting, index, getArray) {
|
||||
@ -9514,13 +9296,16 @@ jQuery(async function () {
|
||||
|
||||
$(document).keyup(function (e) {
|
||||
if (e.key === 'Escape') {
|
||||
if (power_user.auto_save_msg_edits === false) {
|
||||
const isEditVisible = $('#curEditTextarea').is(':visible');
|
||||
if (isEditVisible && power_user.auto_save_msg_edits === false) {
|
||||
closeMessageEditor();
|
||||
$('#send_textarea').focus();
|
||||
return;
|
||||
}
|
||||
if (power_user.auto_save_msg_edits === true) {
|
||||
if (isEditVisible && power_user.auto_save_msg_edits === true) {
|
||||
$(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_done`).click();
|
||||
$('#send_textarea').focus();
|
||||
return;
|
||||
}
|
||||
if (!this_edit_mes_id && $('#mes_stop').is(':visible')) {
|
||||
$('#mes_stop').trigger('click');
|
||||
|
@ -47,8 +47,6 @@ var LeftNavPanel = document.getElementById('left-nav-panel');
|
||||
var WorldInfo = document.getElementById('WorldInfo');
|
||||
|
||||
var SelectedCharacterTab = document.getElementById('rm_button_selected_ch');
|
||||
var AutoConnectCheckbox = document.getElementById('auto-connect-checkbox');
|
||||
var AutoLoadChatCheckbox = document.getElementById('auto-load-chat-checkbox');
|
||||
|
||||
var connection_made = false;
|
||||
var retry_delay = 500;
|
||||
@ -368,7 +366,7 @@ function RA_autoconnect(PrevApi) {
|
||||
setTimeout(RA_autoconnect, 100);
|
||||
return;
|
||||
}
|
||||
if (online_status === 'no_connection' && LoadLocalBool('AutoConnectEnabled')) {
|
||||
if (online_status === 'no_connection' && power_user.auto_connect) {
|
||||
switch (main_api) {
|
||||
case 'kobold':
|
||||
if (api_server && isValidUrl(api_server)) {
|
||||
@ -719,21 +717,19 @@ export function initRossMods() {
|
||||
RA_checkOnlineStatus();
|
||||
}, 100);
|
||||
|
||||
// read the state of AutoConnect and AutoLoadChat.
|
||||
$(AutoConnectCheckbox).prop('checked', LoadLocalBool('AutoConnectEnabled'));
|
||||
$(AutoLoadChatCheckbox).prop('checked', LoadLocalBool('AutoLoadChatEnabled'));
|
||||
if (power_user.auto_load_chat) {
|
||||
RA_autoloadchat();
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
if (LoadLocalBool('AutoLoadChatEnabled') == true) { RA_autoloadchat(); }
|
||||
}, 200);
|
||||
if (power_user.auto_connect) {
|
||||
RA_autoconnect();
|
||||
}
|
||||
|
||||
|
||||
//Autoconnect on page load if enabled, or when api type is changed
|
||||
if (LoadLocalBool('AutoConnectEnabled') == true) { RA_autoconnect(); }
|
||||
$('#main_api').change(function () {
|
||||
var PrevAPI = main_api;
|
||||
setTimeout(() => RA_autoconnect(PrevAPI), 100);
|
||||
});
|
||||
|
||||
$('#api_button').click(function () { setTimeout(RA_checkOnlineStatus, 100); });
|
||||
|
||||
//toggle pin class when lock toggle clicked
|
||||
@ -855,10 +851,6 @@ export function initRossMods() {
|
||||
OpenNavPanels();
|
||||
}, 300);
|
||||
|
||||
//save AutoConnect and AutoLoadChat prefs
|
||||
$(AutoConnectCheckbox).on('change', function () { SaveLocal('AutoConnectEnabled', $(AutoConnectCheckbox).prop('checked')); });
|
||||
$(AutoLoadChatCheckbox).on('change', function () { SaveLocal('AutoLoadChatEnabled', $(AutoLoadChatCheckbox).prop('checked')); });
|
||||
|
||||
$(SelectedCharacterTab).click(function () { SaveLocal('SelectedNavTab', 'rm_button_selected_ch'); });
|
||||
$('#rm_button_characters').click(function () { SaveLocal('SelectedNavTab', 'rm_button_characters'); });
|
||||
|
||||
|
@ -654,7 +654,7 @@ async function updateExtension(extensionName, quiet) {
|
||||
toastr.success('Extension is already up to date');
|
||||
}
|
||||
} else {
|
||||
toastr.success(`Extension ${extensionName} updated to ${data.shortCommitHash}`);
|
||||
toastr.success(`Extension ${extensionName} updated to ${data.shortCommitHash}`, 'Reload the page to apply updates');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
|
@ -10,6 +10,7 @@ export { MODULE_NAME };
|
||||
const MODULE_NAME = 'expressions';
|
||||
const UPDATE_INTERVAL = 2000;
|
||||
const STREAMING_UPDATE_INTERVAL = 6000;
|
||||
const TALKINGCHECK_UPDATE_INTERVAL = 500;
|
||||
const FALLBACK_EXPRESSION = 'joy';
|
||||
const DEFAULT_EXPRESSIONS = [
|
||||
'talkinghead',
|
||||
@ -46,11 +47,17 @@ const DEFAULT_EXPRESSIONS = [
|
||||
let expressionsList = null;
|
||||
let lastCharacter = undefined;
|
||||
let lastMessage = null;
|
||||
let lastTalkingState = false;
|
||||
let lastTalkingStateMessage = null; // last message as seen by `updateTalkingState` (tracked separately, different timer)
|
||||
let spriteCache = {};
|
||||
let inApiCall = false;
|
||||
let lastServerResponseTime = 0;
|
||||
export let lastExpression = {};
|
||||
|
||||
function isTalkingHeadEnabled() {
|
||||
return extension_settings.expressions.talkinghead && !extension_settings.expressions.local;
|
||||
}
|
||||
|
||||
function isVisualNovelMode() {
|
||||
return Boolean(!isMobile() && power_user.waifuMode && getContext().groupId);
|
||||
}
|
||||
@ -381,7 +388,10 @@ function onExpressionsShowDefaultInput() {
|
||||
}
|
||||
}
|
||||
|
||||
async function unloadLiveChar() {
|
||||
/**
|
||||
* Stops animating a talkinghead.
|
||||
*/
|
||||
async function unloadTalkingHead() {
|
||||
if (!modules.includes('talkinghead')) {
|
||||
console.debug('talkinghead module is disabled');
|
||||
return;
|
||||
@ -400,7 +410,10 @@ async function unloadLiveChar() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLiveChar() {
|
||||
/**
|
||||
* Posts `talkinghead.png` of the current character to the talkinghead module in SillyTavern-extras, to start animating it.
|
||||
*/
|
||||
async function loadTalkingHead() {
|
||||
if (!modules.includes('talkinghead')) {
|
||||
console.debug('talkinghead module is disabled');
|
||||
return;
|
||||
@ -409,6 +422,8 @@ async function loadLiveChar() {
|
||||
const spriteFolderName = getSpriteFolderName();
|
||||
|
||||
const talkingheadPath = `/characters/${encodeURIComponent(spriteFolderName)}/talkinghead.png`;
|
||||
const emotionsSettingsPath = `/characters/${encodeURIComponent(spriteFolderName)}/_emotions.json`;
|
||||
const animatorSettingsPath = `/characters/${encodeURIComponent(spriteFolderName)}/_animator.json`;
|
||||
|
||||
try {
|
||||
const spriteResponse = await fetch(talkingheadPath);
|
||||
@ -437,6 +452,69 @@ async function loadLiveChar() {
|
||||
const loadResponseText = await loadResponse.text();
|
||||
console.log(`Load talkinghead response: ${loadResponseText}`);
|
||||
|
||||
// Optional: per-character emotion templates
|
||||
let emotionsSettings;
|
||||
try {
|
||||
const emotionsResponse = await fetch(emotionsSettingsPath);
|
||||
if (emotionsResponse.ok) {
|
||||
emotionsSettings = await emotionsResponse.json();
|
||||
console.log(`Loaded ${emotionsSettingsPath}`);
|
||||
} else {
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
emotionsSettings = {}; // blank -> use server defaults (to unload the previous character's customizations)
|
||||
console.log(`No valid config at ${emotionsSettingsPath}, using server defaults`);
|
||||
}
|
||||
try {
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/talkinghead/load_emotion_templates';
|
||||
const apiResult = await doExtrasFetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Bypass-Tunnel-Reminder': 'bypass',
|
||||
},
|
||||
body: JSON.stringify(emotionsSettings),
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
// it's ok if not supported
|
||||
console.log('Failed to send _emotions.json (backend too old?), ignoring');
|
||||
}
|
||||
|
||||
// Optional: per-character animator and postprocessor config
|
||||
let animatorSettings;
|
||||
try {
|
||||
const animatorResponse = await fetch(animatorSettingsPath);
|
||||
if (animatorResponse.ok) {
|
||||
animatorSettings = await animatorResponse.json();
|
||||
console.log(`Loaded ${animatorSettingsPath}`);
|
||||
} else {
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
animatorSettings = {}; // blank -> use server defaults (to unload the previous character's customizations)
|
||||
console.log(`No valid config at ${animatorSettingsPath}, using server defaults`);
|
||||
}
|
||||
try {
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/talkinghead/load_animator_settings';
|
||||
const apiResult = await doExtrasFetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Bypass-Tunnel-Reminder': 'bypass',
|
||||
},
|
||||
body: JSON.stringify(animatorSettings),
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
// it's ok if not supported
|
||||
console.log('Failed to send _animator.json (backend too old?), ignoring');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error loading talkinghead image: ${talkingheadPath} - ${error}`);
|
||||
}
|
||||
@ -450,7 +528,7 @@ function handleImageChange() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (extension_settings.expressions.talkinghead && !extension_settings.expressions.local) {
|
||||
if (isTalkingHeadEnabled()) {
|
||||
// Method get IP of endpoint
|
||||
const talkingheadResultFeedSrc = `${getApiUrl()}/api/talkinghead/result_feed`;
|
||||
$('#expression-holder').css({ display: '' });
|
||||
@ -559,9 +637,10 @@ async function moduleWorker() {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastMessageChanged = !((lastCharacter === context.characterId || lastCharacter === context.groupId) && lastMessage === currentLastMessage.mes);
|
||||
|
||||
// check if last message changed
|
||||
if ((lastCharacter === context.characterId || lastCharacter === context.groupId)
|
||||
&& lastMessage === currentLastMessage.mes) {
|
||||
if (!lastMessageChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -611,21 +690,81 @@ async function moduleWorker() {
|
||||
}
|
||||
}
|
||||
|
||||
async function talkingHeadCheck() {
|
||||
/**
|
||||
* Starts/stops talkinghead talking animation.
|
||||
*
|
||||
* Talking starts only when all the following conditions are met:
|
||||
* - The LLM is currently streaming its output.
|
||||
* - The AI's current last message is non-empty, and also not just '...' (as produced by a swipe).
|
||||
* - The AI's current last message has changed from what we saw during the previous call.
|
||||
*
|
||||
* In all other cases, talking stops.
|
||||
*
|
||||
* A talkinghead API call is made only when the talking state changes.
|
||||
*/
|
||||
async function updateTalkingState() {
|
||||
// Don't bother if talkinghead is disabled or not loaded.
|
||||
if (!isTalkingHeadEnabled() || !modules.includes('talkinghead')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const context = getContext();
|
||||
const currentLastMessage = getLastCharacterMessage();
|
||||
|
||||
try {
|
||||
// TODO: Not sure if we need also "&& !context.groupId" here - the classify check in `moduleWorker`
|
||||
// (that similarly checks the streaming processor state) does that for some reason.
|
||||
// Talkinghead isn't currently designed to work with groups.
|
||||
const lastMessageChanged = !((lastCharacter === context.characterId || lastCharacter === context.groupId) && lastTalkingStateMessage === currentLastMessage.mes);
|
||||
const url = new URL(getApiUrl());
|
||||
let newTalkingState;
|
||||
if (context.streamingProcessor && !context.streamingProcessor.isFinished &&
|
||||
currentLastMessage.mes.length !== 0 && currentLastMessage.mes !== '...' && lastMessageChanged) {
|
||||
url.pathname = '/api/talkinghead/start_talking';
|
||||
newTalkingState = true;
|
||||
} else {
|
||||
url.pathname = '/api/talkinghead/stop_talking';
|
||||
newTalkingState = false;
|
||||
}
|
||||
try {
|
||||
// Call the talkinghead API only if the talking state changed.
|
||||
if (newTalkingState !== lastTalkingState) {
|
||||
console.debug(`updateTalkingState: calling ${url.pathname}`);
|
||||
await doExtrasFetch(url);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
// it's ok if not supported
|
||||
}
|
||||
finally {
|
||||
lastTalkingState = newTalkingState;
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
// console.log(error);
|
||||
}
|
||||
finally {
|
||||
lastTalkingStateMessage = currentLastMessage.mes;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the current character has a talkinghead image available.
|
||||
* @returns {Promise<boolean>} True if the character has a talkinghead image available, false otherwise.
|
||||
*/
|
||||
async function isTalkingHeadAvailable() {
|
||||
let spriteFolderName = getSpriteFolderName();
|
||||
|
||||
try {
|
||||
await validateImages(spriteFolderName);
|
||||
|
||||
let talkingheadObj = spriteCache[spriteFolderName].find(obj => obj.label === 'talkinghead');
|
||||
let talkingheadPath_f = talkingheadObj ? talkingheadObj.path : null;
|
||||
let talkingheadPath = talkingheadObj ? talkingheadObj.path : null;
|
||||
|
||||
if (talkingheadPath_f != null) {
|
||||
//console.log("talkingheadPath_f " + talkingheadPath_f);
|
||||
if (talkingheadPath != null) {
|
||||
return true;
|
||||
} else {
|
||||
//console.log("talkingheadPath_f is null");
|
||||
unloadLiveChar();
|
||||
await unloadTalkingHead();
|
||||
return false;
|
||||
}
|
||||
} catch (err) {
|
||||
@ -647,22 +786,22 @@ function getSpriteFolderName(characterMessage = null, characterName = null) {
|
||||
return spriteFolderName;
|
||||
}
|
||||
|
||||
function setTalkingHeadState(switch_var) {
|
||||
extension_settings.expressions.talkinghead = switch_var; // Store setting
|
||||
function setTalkingHeadState(newState) {
|
||||
extension_settings.expressions.talkinghead = newState; // Store setting
|
||||
saveSettingsDebounced();
|
||||
|
||||
if (extension_settings.expressions.local) {
|
||||
return;
|
||||
}
|
||||
|
||||
talkingHeadCheck().then(result => {
|
||||
isTalkingHeadAvailable().then(result => {
|
||||
if (result) {
|
||||
//console.log("talkinghead exists!");
|
||||
|
||||
if (extension_settings.expressions.talkinghead) {
|
||||
loadLiveChar();
|
||||
loadTalkingHead();
|
||||
} else {
|
||||
unloadLiveChar();
|
||||
unloadTalkingHead();
|
||||
}
|
||||
handleImageChange(); // Change image as needed
|
||||
|
||||
@ -732,22 +871,29 @@ async function setSpriteSlashCommand(_, spriteId) {
|
||||
|
||||
spriteId = spriteId.trim().toLowerCase();
|
||||
|
||||
// In talkinghead mode, don't check for the existence of the sprite
|
||||
// (emotion names are the same as for sprites, but it only needs "talkinghead.png").
|
||||
const currentLastMessage = getLastCharacterMessage();
|
||||
const spriteFolderName = getSpriteFolderName(currentLastMessage, currentLastMessage.name);
|
||||
await validateImages(spriteFolderName);
|
||||
let label = spriteId;
|
||||
if (!isTalkingHeadEnabled()) {
|
||||
await validateImages(spriteFolderName);
|
||||
|
||||
// Fuzzy search for sprite
|
||||
const fuse = new Fuse(spriteCache[spriteFolderName], { keys: ['label'] });
|
||||
const results = fuse.search(spriteId);
|
||||
const spriteItem = results[0]?.item;
|
||||
// Fuzzy search for sprite
|
||||
const fuse = new Fuse(spriteCache[spriteFolderName], { keys: ['label'] });
|
||||
const results = fuse.search(spriteId);
|
||||
const spriteItem = results[0]?.item;
|
||||
|
||||
if (!spriteItem) {
|
||||
console.log('No sprite found for search term ' + spriteId);
|
||||
return;
|
||||
if (!spriteItem) {
|
||||
console.log('No sprite found for search term ' + spriteId);
|
||||
return;
|
||||
}
|
||||
|
||||
label = spriteItem.label;
|
||||
}
|
||||
|
||||
const vnMode = isVisualNovelMode();
|
||||
await sendExpressionCall(spriteFolderName, spriteItem.label, true, vnMode);
|
||||
await sendExpressionCall(spriteFolderName, label, true, vnMode);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -998,7 +1144,7 @@ async function getExpressionsList() {
|
||||
}
|
||||
|
||||
async function setExpression(character, expression, force) {
|
||||
if (extension_settings.expressions.local || !extension_settings.expressions.talkinghead) {
|
||||
if (!isTalkingHeadEnabled()) {
|
||||
console.debug('entered setExpressions');
|
||||
await validateImages(character);
|
||||
const img = $('img.expression');
|
||||
@ -1109,24 +1255,38 @@ async function setExpression(character, expression, force) {
|
||||
document.getElementById('expression-holder').style.display = '';
|
||||
|
||||
} else {
|
||||
|
||||
|
||||
talkingHeadCheck().then(result => {
|
||||
// Set the talkinghead emotion to the specified expression
|
||||
// TODO: For now, talkinghead emote only supported when VN mode is off; see also updateVisualNovelMode.
|
||||
try {
|
||||
let result = await isTalkingHeadAvailable();
|
||||
if (result) {
|
||||
// Find the <img> element with id="expression-image" and class="expression"
|
||||
const imgElement = document.querySelector('img#expression-image.expression');
|
||||
//console.log("searching");
|
||||
if (imgElement && imgElement instanceof HTMLImageElement) {
|
||||
//console.log("setting value");
|
||||
imgElement.src = getApiUrl() + '/api/talkinghead/result_feed';
|
||||
}
|
||||
|
||||
} else {
|
||||
//console.log("The fetch failed!");
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/talkinghead/set_emotion';
|
||||
await doExtrasFetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ emotion_name: expression }),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
catch (error) {
|
||||
// `set_emotion` is not present in old versions, so let it 404.
|
||||
}
|
||||
|
||||
try {
|
||||
// Find the <img> element with id="expression-image" and class="expression"
|
||||
const imgElement = document.querySelector('img#expression-image.expression');
|
||||
//console.log("searching");
|
||||
if (imgElement && imgElement instanceof HTMLImageElement) {
|
||||
//console.log("setting value");
|
||||
imgElement.src = getApiUrl() + '/api/talkinghead/result_feed';
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
//console.log("The fetch failed!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1247,6 +1407,11 @@ async function onClickExpressionUpload(event) {
|
||||
|
||||
// Reset the input
|
||||
e.target.form.reset();
|
||||
|
||||
// In talkinghead mode, when a new talkinghead image is uploaded, refresh the live char.
|
||||
if (isTalkingHeadEnabled() && id === 'talkinghead') {
|
||||
await loadTalkingHead();
|
||||
}
|
||||
};
|
||||
|
||||
$('#expression_upload')
|
||||
@ -1473,6 +1638,11 @@ function setExpressionOverrideHtml(forceClear = false) {
|
||||
const updateFunction = wrapper.update.bind(wrapper);
|
||||
setInterval(updateFunction, UPDATE_INTERVAL);
|
||||
moduleWorker();
|
||||
// For setting the talkinghead talking animation on/off quickly enough for realtime use, we need another timer on a shorter schedule.
|
||||
const wrapperTalkingState = new ModuleWorkerWrapper(updateTalkingState);
|
||||
const updateTalkingStateFunction = wrapperTalkingState.update.bind(wrapperTalkingState);
|
||||
setInterval(updateTalkingStateFunction, TALKINGCHECK_UPDATE_INTERVAL);
|
||||
updateTalkingState();
|
||||
dragElement($('#expression-holder'));
|
||||
eventSource.on(event_types.CHAT_CHANGED, () => {
|
||||
// character changed
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
characters,
|
||||
getRequestHeaders,
|
||||
} from '../../../script.js';
|
||||
import { selected_group } from '../../group-chats.js';
|
||||
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';
|
||||
@ -416,7 +416,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);
|
||||
|
||||
function showGalleryCommand(args) {
|
||||
showCharGallery();
|
||||
}
|
||||
|
||||
async function listGalleryCommand(args) {
|
||||
try {
|
||||
let url = args.char ?? (args.group ? groups.find(it=>it.name == args.group)?.id : null) ?? (selected_group || this_chid);
|
||||
if (!args.char && !args.group && !selected_group && this_chid) {
|
||||
const char = characters[this_chid];
|
||||
url = char.avatar.replace('.png', '');
|
||||
}
|
||||
|
||||
const items = await getGalleryItems(url);
|
||||
return JSON.stringify(items.map(it=>it.src));
|
||||
|
||||
} catch (err) {
|
||||
console.trace();
|
||||
console.error(err);
|
||||
}
|
||||
return JSON.stringify([]);
|
||||
}
|
||||
|
451
public/scripts/extensions/quick-reply/api/QuickReplyApi.js
Normal file
451
public/scripts/extensions/quick-reply/api/QuickReplyApi.js
Normal file
@ -0,0 +1,451 @@
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { QuickReply } from '../src/QuickReply.js';
|
||||
import { QuickReplyContextLink } from '../src/QuickReplyContextLink.js';
|
||||
import { QuickReplySet } from '../src/QuickReplySet.js';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { QuickReplySettings } from '../src/QuickReplySettings.js';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { SettingsUi } from '../src/ui/SettingsUi.js';
|
||||
|
||||
export class QuickReplyApi {
|
||||
/**@type {QuickReplySettings}*/ settings;
|
||||
/**@type {SettingsUi}*/ settingsUi;
|
||||
|
||||
|
||||
|
||||
|
||||
constructor(/**@type {QuickReplySettings}*/settings, /**@type {SettingsUi}*/settingsUi) {
|
||||
this.settings = settings;
|
||||
this.settingsUi = settingsUi;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Finds and returns an existing Quick Reply Set by its name.
|
||||
*
|
||||
* @param {String} name name of the quick reply set
|
||||
* @returns the quick reply set, or undefined if not found
|
||||
*/
|
||||
getSetByName(name) {
|
||||
return QuickReplySet.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and returns an existing Quick Reply by its set's name and its label.
|
||||
*
|
||||
* @param {String} setName name of the quick reply set
|
||||
* @param {String} label label of the quick reply
|
||||
* @returns the quick reply, or undefined if not found
|
||||
*/
|
||||
getQrByLabel(setName, label) {
|
||||
const set = this.getSetByName(setName);
|
||||
if (!set) return;
|
||||
return set.qrList.find(it=>it.label == label);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Executes a quick reply by its index and returns the result.
|
||||
*
|
||||
* @param {Number} idx the index (zero-based) of the quick reply to execute
|
||||
* @returns the return value of the quick reply, or undefined if not found
|
||||
*/
|
||||
async executeQuickReplyByIndex(idx) {
|
||||
const qr = [...this.settings.config.setList, ...(this.settings.chatConfig?.setList ?? [])]
|
||||
.map(it=>it.set.qrList)
|
||||
.flat()[idx]
|
||||
;
|
||||
if (qr) {
|
||||
return await qr.onExecute();
|
||||
} else {
|
||||
throw new Error(`No quick reply at index "${idx}"`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes an existing quick reply.
|
||||
*
|
||||
* @param {String} setName name of the existing quick reply set
|
||||
* @param {String} label label of the existing quick reply (text on the button)
|
||||
* @param {Object} [args] optional arguments
|
||||
*/
|
||||
async executeQuickReply(setName, label, args = {}) {
|
||||
const qr = this.getQrByLabel(setName, label);
|
||||
if (!qr) {
|
||||
throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);
|
||||
}
|
||||
return await qr.execute(args);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds or removes a quick reply set to the list of globally active quick reply sets.
|
||||
*
|
||||
* @param {String} name the name of the set
|
||||
* @param {Boolean} isVisible whether to show the set's buttons or not
|
||||
*/
|
||||
toggleGlobalSet(name, isVisible = true) {
|
||||
const set = this.getSetByName(name);
|
||||
if (!set) {
|
||||
throw new Error(`No quick reply set with name "${name}" found.`);
|
||||
}
|
||||
if (this.settings.config.hasSet(set)) {
|
||||
this.settings.config.removeSet(set);
|
||||
} else {
|
||||
this.settings.config.addSet(set, isVisible);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a quick reply set to the list of globally active quick reply sets.
|
||||
*
|
||||
* @param {String} name the name of the set
|
||||
* @param {Boolean} isVisible whether to show the set's buttons or not
|
||||
*/
|
||||
addGlobalSet(name, isVisible = true) {
|
||||
const set = this.getSetByName(name);
|
||||
if (!set) {
|
||||
throw new Error(`No quick reply set with name "${name}" found.`);
|
||||
}
|
||||
this.settings.config.addSet(set, isVisible);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a quick reply set from the list of globally active quick reply sets.
|
||||
*
|
||||
* @param {String} name the name of the set
|
||||
*/
|
||||
removeGlobalSet(name) {
|
||||
const set = this.getSetByName(name);
|
||||
if (!set) {
|
||||
throw new Error(`No quick reply set with name "${name}" found.`);
|
||||
}
|
||||
this.settings.config.removeSet(set);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds or removes a quick reply set to the list of the current chat's active quick reply sets.
|
||||
*
|
||||
* @param {String} name the name of the set
|
||||
* @param {Boolean} isVisible whether to show the set's buttons or not
|
||||
*/
|
||||
toggleChatSet(name, isVisible = true) {
|
||||
if (!this.settings.chatConfig) return;
|
||||
const set = this.getSetByName(name);
|
||||
if (!set) {
|
||||
throw new Error(`No quick reply set with name "${name}" found.`);
|
||||
}
|
||||
if (this.settings.chatConfig.hasSet(set)) {
|
||||
this.settings.chatConfig.removeSet(set);
|
||||
} else {
|
||||
this.settings.chatConfig.addSet(set, isVisible);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a quick reply set to the list of the current chat's active quick reply sets.
|
||||
*
|
||||
* @param {String} name the name of the set
|
||||
* @param {Boolean} isVisible whether to show the set's buttons or not
|
||||
*/
|
||||
addChatSet(name, isVisible = true) {
|
||||
if (!this.settings.chatConfig) return;
|
||||
const set = this.getSetByName(name);
|
||||
if (!set) {
|
||||
throw new Error(`No quick reply set with name "${name}" found.`);
|
||||
}
|
||||
this.settings.chatConfig.addSet(set, isVisible);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a quick reply set from the list of the current chat's active quick reply sets.
|
||||
*
|
||||
* @param {String} name the name of the set
|
||||
*/
|
||||
removeChatSet(name) {
|
||||
if (!this.settings.chatConfig) return;
|
||||
const set = this.getSetByName(name);
|
||||
if (!set) {
|
||||
throw new Error(`No quick reply set with name "${name}" found.`);
|
||||
}
|
||||
this.settings.chatConfig.removeSet(set);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new quick reply in an existing quick reply set.
|
||||
*
|
||||
* @param {String} setName name of the quick reply set to insert the new quick reply into
|
||||
* @param {String} label label for the new quick reply (text on the button)
|
||||
* @param {Object} [props]
|
||||
* @param {String} [props.message] the message to be sent or slash command to be executed by the new quick reply
|
||||
* @param {String} [props.title] the title / tooltip to be shown on the quick reply button
|
||||
* @param {Boolean} [props.isHidden] whether to hide or show the button
|
||||
* @param {Boolean} [props.executeOnStartup] whether to execute the quick reply when SillyTavern starts
|
||||
* @param {Boolean} [props.executeOnUser] whether to execute the quick reply after a user has sent a message
|
||||
* @param {Boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message
|
||||
* @param {Boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded
|
||||
* @returns {QuickReply} the new quick reply
|
||||
*/
|
||||
createQuickReply(setName, label, {
|
||||
message,
|
||||
title,
|
||||
isHidden,
|
||||
executeOnStartup,
|
||||
executeOnUser,
|
||||
executeOnAi,
|
||||
executeOnChatChange,
|
||||
} = {}) {
|
||||
const set = this.getSetByName(setName);
|
||||
if (!set) {
|
||||
throw new Error(`No quick reply set with named "${setName}" found.`);
|
||||
}
|
||||
const qr = set.addQuickReply();
|
||||
qr.label = label ?? '';
|
||||
qr.message = message ?? '';
|
||||
qr.title = title ?? '';
|
||||
qr.isHidden = isHidden ?? false;
|
||||
qr.executeOnStartup = executeOnStartup ?? false;
|
||||
qr.executeOnUser = executeOnUser ?? false;
|
||||
qr.executeOnAi = executeOnAi ?? false;
|
||||
qr.executeOnChatChange = executeOnChatChange ?? false;
|
||||
qr.onUpdate();
|
||||
return qr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing quick reply.
|
||||
*
|
||||
* @param {String} setName name of the existing quick reply set
|
||||
* @param {String} label label of the existing quick reply (text on the button)
|
||||
* @param {Object} [props]
|
||||
* @param {String} [props.newLabel] new label for quick reply (text on the button)
|
||||
* @param {String} [props.message] the message to be sent or slash command to be executed by the quick reply
|
||||
* @param {String} [props.title] the title / tooltip to be shown on the quick reply button
|
||||
* @param {Boolean} [props.isHidden] whether to hide or show the button
|
||||
* @param {Boolean} [props.executeOnStartup] whether to execute the quick reply when SillyTavern starts
|
||||
* @param {Boolean} [props.executeOnUser] whether to execute the quick reply after a user has sent a message
|
||||
* @param {Boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message
|
||||
* @param {Boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded
|
||||
* @returns {QuickReply} the altered quick reply
|
||||
*/
|
||||
updateQuickReply(setName, label, {
|
||||
newLabel,
|
||||
message,
|
||||
title,
|
||||
isHidden,
|
||||
executeOnStartup,
|
||||
executeOnUser,
|
||||
executeOnAi,
|
||||
executeOnChatChange,
|
||||
} = {}) {
|
||||
const qr = this.getQrByLabel(setName, label);
|
||||
if (!qr) {
|
||||
throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);
|
||||
}
|
||||
qr.label = newLabel ?? qr.label;
|
||||
qr.message = message ?? qr.message;
|
||||
qr.title = title ?? qr.title;
|
||||
qr.isHidden = isHidden ?? qr.isHidden;
|
||||
qr.executeOnStartup = executeOnStartup ?? qr.executeOnStartup;
|
||||
qr.executeOnUser = executeOnUser ?? qr.executeOnUser;
|
||||
qr.executeOnAi = executeOnAi ?? qr.executeOnAi;
|
||||
qr.executeOnChatChange = executeOnChatChange ?? qr.executeOnChatChange;
|
||||
qr.onUpdate();
|
||||
return qr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an existing quick reply.
|
||||
*
|
||||
* @param {String} setName name of the existing quick reply set
|
||||
* @param {String} label label of the existing quick reply (text on the button)
|
||||
*/
|
||||
deleteQuickReply(setName, label) {
|
||||
const qr = this.getQrByLabel(setName, label);
|
||||
if (!qr) {
|
||||
throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);
|
||||
}
|
||||
qr.delete();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds an existing quick reply set as a context menu to an existing quick reply.
|
||||
*
|
||||
* @param {String} setName name of the existing quick reply set containing the quick reply
|
||||
* @param {String} label label of the existing quick reply
|
||||
* @param {String} contextSetName name of the existing quick reply set to be used as a context menu
|
||||
* @param {Boolean} isChained whether or not to chain the context menu quick replies
|
||||
*/
|
||||
createContextItem(setName, label, contextSetName, isChained = false) {
|
||||
const qr = this.getQrByLabel(setName, label);
|
||||
const set = this.getSetByName(contextSetName);
|
||||
if (!qr) {
|
||||
throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);
|
||||
}
|
||||
if (!set) {
|
||||
throw new Error(`No quick reply set with name "${contextSetName}" found.`);
|
||||
}
|
||||
const cl = new QuickReplyContextLink();
|
||||
cl.set = set;
|
||||
cl.isChained = isChained;
|
||||
qr.addContextLink(cl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a quick reply set from a quick reply's context menu.
|
||||
*
|
||||
* @param {String} setName name of the existing quick reply set containing the quick reply
|
||||
* @param {String} label label of the existing quick reply
|
||||
* @param {String} contextSetName name of the existing quick reply set to be used as a context menu
|
||||
*/
|
||||
deleteContextItem(setName, label, contextSetName) {
|
||||
const qr = this.getQrByLabel(setName, label);
|
||||
const set = this.getSetByName(contextSetName);
|
||||
if (!qr) {
|
||||
throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);
|
||||
}
|
||||
if (!set) {
|
||||
throw new Error(`No quick reply set with name "${contextSetName}" found.`);
|
||||
}
|
||||
qr.removeContextLink(set.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all entries from a quick reply's context menu.
|
||||
*
|
||||
* @param {String} setName name of the existing quick reply set containing the quick reply
|
||||
* @param {String} label label of the existing quick reply
|
||||
*/
|
||||
clearContextMenu(setName, label) {
|
||||
const qr = this.getQrByLabel(setName, label);
|
||||
if (!qr) {
|
||||
throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);
|
||||
}
|
||||
qr.clearContextLinks();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a new quick reply set.
|
||||
*
|
||||
* @param {String} name name of the new quick reply set
|
||||
* @param {Object} [props]
|
||||
* @param {Boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box
|
||||
* @param {Boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input
|
||||
* @param {Boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply
|
||||
* @returns {Promise<QuickReplySet>} the new quick reply set
|
||||
*/
|
||||
async createSet(name, {
|
||||
disableSend,
|
||||
placeBeforeInput,
|
||||
injectInput,
|
||||
} = {}) {
|
||||
const set = new QuickReplySet();
|
||||
set.name = name;
|
||||
set.disableSend = disableSend ?? false;
|
||||
set.placeBeforeInput = placeBeforeInput ?? false;
|
||||
set.injectInput = injectInput ?? false;
|
||||
const oldSet = this.getSetByName(name);
|
||||
if (oldSet) {
|
||||
QuickReplySet.list.splice(QuickReplySet.list.indexOf(oldSet), 1, set);
|
||||
} else {
|
||||
const idx = QuickReplySet.list.findIndex(it=>it.name.localeCompare(name) == 1);
|
||||
if (idx > -1) {
|
||||
QuickReplySet.list.splice(idx, 0, set);
|
||||
} else {
|
||||
QuickReplySet.list.push(set);
|
||||
}
|
||||
}
|
||||
await set.save();
|
||||
this.settingsUi.rerender();
|
||||
return set;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing quick reply set.
|
||||
*
|
||||
* @param {String} name name of the existing quick reply set
|
||||
* @param {Object} [props]
|
||||
* @param {Boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box
|
||||
* @param {Boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input
|
||||
* @param {Boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply
|
||||
* @returns {Promise<QuickReplySet>} the altered quick reply set
|
||||
*/
|
||||
async updateSet(name, {
|
||||
disableSend,
|
||||
placeBeforeInput,
|
||||
injectInput,
|
||||
} = {}) {
|
||||
const set = this.getSetByName(name);
|
||||
if (!set) {
|
||||
throw new Error(`No quick reply set with name "${name}" found.`);
|
||||
}
|
||||
set.disableSend = disableSend ?? false;
|
||||
set.placeBeforeInput = placeBeforeInput ?? false;
|
||||
set.injectInput = injectInput ?? false;
|
||||
await set.save();
|
||||
this.settingsUi.rerender();
|
||||
return set;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an existing quick reply set.
|
||||
*
|
||||
* @param {String} name name of the existing quick reply set
|
||||
*/
|
||||
async deleteSet(name) {
|
||||
const set = this.getSetByName(name);
|
||||
if (!set) {
|
||||
throw new Error(`No quick reply set with name "${name}" found.`);
|
||||
}
|
||||
await set.delete();
|
||||
this.settingsUi.rerender();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets a list of all quick reply sets.
|
||||
*
|
||||
* @returns array with the names of all quick reply sets
|
||||
*/
|
||||
listSets() {
|
||||
return QuickReplySet.list.map(it=>it.name);
|
||||
}
|
||||
/**
|
||||
* Gets a list of all globally active quick reply sets.
|
||||
*
|
||||
* @returns array with the names of all quick reply sets
|
||||
*/
|
||||
listGlobalSets() {
|
||||
return this.settings.config.setList.map(it=>it.set.name);
|
||||
}
|
||||
/**
|
||||
* Gets a list of all quick reply sets activated by the current chat.
|
||||
*
|
||||
* @returns array with the names of all quick reply sets
|
||||
*/
|
||||
listChatSets() {
|
||||
return this.settings.chatConfig?.setList?.flatMap(it=>it.set.name) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of all quick replies in the quick reply set.
|
||||
*
|
||||
* @param {String} setName name of the existing quick reply set
|
||||
* @returns array with the labels of this set's quick replies
|
||||
*/
|
||||
listQuickReplies(setName) {
|
||||
const set = this.getSetByName(setName);
|
||||
if (!set) {
|
||||
throw new Error(`No quick reply set with name "${name}" found.`);
|
||||
}
|
||||
return set.qrList.map(it=>it.label);
|
||||
}
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
<div id="quickReply_contextMenuEditor_template">
|
||||
<div class="quickReply_contextMenuEditor">
|
||||
<h3><strong>Context Menu Editor</strong></h3>
|
||||
<div id="quickReply_contextMenuEditor_content">
|
||||
<template id="quickReply_contextMenuEditor_itemTemplate">
|
||||
<div class="quickReplyContextMenuEditor_item flex-container alignitemscenter" data-order="0">
|
||||
<span class="drag-handle ui-sortable-handle">☰</span>
|
||||
<select class="quickReply_contextMenuEditor_preset"></select>
|
||||
<label class="flex-container" title="When enabled, the current Quick Reply will be sent together with (before) the clicked QR from the context menu.">
|
||||
Chaining:
|
||||
<input type="checkbox" class="quickReply_contextMenuEditor_chaining">
|
||||
</label>
|
||||
<span class="quickReply_contextMenuEditor_remove menu_button menu_button_icon fa-solid fa-trash-can" title="Remove entry"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="quickReply_contextMenuEditor_actions">
|
||||
<span id="quickReply_contextMenuEditor_addPreset" class="menu_button menu_button_icon fa-solid fa-plus" title="Add preset to context menu"></span>
|
||||
</div>
|
||||
<h3><strong>Auto-Execute</strong></h3>
|
||||
<div class="flex-container flexFlowColumn">
|
||||
<label class="checkbox_label" for="quickReply_hidden">
|
||||
<input type="checkbox" id="quickReply_hidden" >
|
||||
<span><i class="fa-solid fa-fw fa-eye-slash"></i> Invisible (auto-execute only)</span>
|
||||
</label>
|
||||
<label class="checkbox_label" for="quickReply_autoExecute_appStartup">
|
||||
<input type="checkbox" id="quickReply_autoExecute_appStartup" >
|
||||
<span><i class="fa-solid fa-fw fa-rocket"></i> Execute on app startup</span>
|
||||
</label>
|
||||
<label class="checkbox_label" for="quickReply_autoExecute_userMessage">
|
||||
<input type="checkbox" id="quickReply_autoExecute_userMessage" >
|
||||
<span><i class="fa-solid fa-fw fa-user"></i> Execute on user message</span>
|
||||
</label>
|
||||
<label class="checkbox_label" for="quickReply_autoExecute_botMessage">
|
||||
<input type="checkbox" id="quickReply_autoExecute_botMessage" >
|
||||
<span><i class="fa-solid fa-fw fa-robot"></i> Execute on AI message</span>
|
||||
</label>
|
||||
<label class="checkbox_label" for="quickReply_autoExecute_chatLoad">
|
||||
<input type="checkbox" id="quickReply_autoExecute_chatLoad" >
|
||||
<span><i class="fa-solid fa-fw fa-message"></i> Execute on opening chat</span>
|
||||
</label>
|
||||
</div>
|
||||
<h3><strong>UI Options</strong></h3>
|
||||
<div class="flex-container flexFlowColumn">
|
||||
<label for="quickReply_ui_title">Title (tooltip, leave empty to show the message or /command)</label>
|
||||
<input type="text" class="text_pole" id="quickReply_ui_title">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
83
public/scripts/extensions/quick-reply/html/qrEditor.html
Normal file
83
public/scripts/extensions/quick-reply/html/qrEditor.html
Normal file
@ -0,0 +1,83 @@
|
||||
<div id="qr--modalEditor">
|
||||
<div id="qr--main">
|
||||
<h3>Labels and Message</h3>
|
||||
<div class="qr--labels">
|
||||
<label>
|
||||
<span class="qr--labelText">Label</span>
|
||||
<input type="text" class="text_pole" id="qr--modal-label">
|
||||
</label>
|
||||
<label>
|
||||
<span class="qr--labelText">Title</span>
|
||||
<small class="qr--labelHint">(tooltip, leave empty to show message or /command)</small>
|
||||
<input type="text" class="text_pole" id="qr--modal-title">
|
||||
</label>
|
||||
</div>
|
||||
<div class="qr--modal-messageContainer">
|
||||
<label for="qr--modal-message">Message / Command:</label>
|
||||
<textarea class="monospace" id="qr--modal-message"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div id="qr--qrOptions">
|
||||
<h3>Context Menu</h3>
|
||||
<div id="qr--ctxEditor">
|
||||
<template id="qr--ctxItem">
|
||||
<div class="qr--ctxItem" data-order="0">
|
||||
<div class="drag-handle ui-sortable-handle">☰</div>
|
||||
<select class="qr--set"></select>
|
||||
<label class="qr--isChainedLabel checkbox_label" title="When enabled, the current Quick Reply will be sent together with (before) the clicked QR from the context menu.">
|
||||
Chaining:
|
||||
<input type="checkbox" class="qr--isChained">
|
||||
</label>
|
||||
<div class="qr--delete menu_button menu_button_icon fa-solid fa-trash-can" title="Remove entry"></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="qr--ctxEditorActions">
|
||||
<span id="qr--ctxAdd" class="menu_button menu_button_icon fa-solid fa-plus" title="Add quick reply set to context menu"></span>
|
||||
</div>
|
||||
|
||||
|
||||
<h3>Auto-Execute</h3>
|
||||
<div class="flex-container flexFlowColumn">
|
||||
<label class="checkbox_label" title="Prevent this quick reply from triggering other auto-executed quick replies while auto-executing (i.e., prevent recursive auto-execution)">
|
||||
<input type="checkbox" id="qr--preventAutoExecute" >
|
||||
<span><i class="fa-solid fa-fw fa-plane-slash"></i> Don't trigger auto-execute</span>
|
||||
</label>
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="qr--isHidden" >
|
||||
<span><i class="fa-solid fa-fw fa-eye-slash"></i> Invisible (auto-execute only)</span>
|
||||
</label>
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="qr--executeOnStartup" >
|
||||
<span><i class="fa-solid fa-fw fa-rocket"></i> Execute on app startup</span>
|
||||
</label>
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="qr--executeOnUser" >
|
||||
<span><i class="fa-solid fa-fw fa-user"></i> Execute on user message</span>
|
||||
</label>
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="qr--executeOnAi" >
|
||||
<span><i class="fa-solid fa-fw fa-robot"></i> Execute on AI message</span>
|
||||
</label>
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="qr--executeOnChatChange" >
|
||||
<span><i class="fa-solid fa-fw fa-message"></i> Execute on opening chat</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
71
public/scripts/extensions/quick-reply/html/settings.html
Normal file
71
public/scripts/extensions/quick-reply/html/settings.html
Normal file
@ -0,0 +1,71 @@
|
||||
<div id="qr--settings">
|
||||
<div class="inline-drawer">
|
||||
<div class="inline-drawer-toggle inline-drawer-header">
|
||||
<strong>Quick Reply</strong>
|
||||
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
||||
</div>
|
||||
<div class="inline-drawer-content">
|
||||
<label class="flex-container">
|
||||
<input type="checkbox" id="qr--isEnabled"> Enable Quick Replies
|
||||
</label>
|
||||
<label class="flex-container">
|
||||
<input type="checkbox" id="qr--isCombined"> Combine buttons from all active sets
|
||||
</label>
|
||||
|
||||
<hr>
|
||||
|
||||
<div id="qr--global">
|
||||
<div class="qr--head">
|
||||
<div class="qr--title">Global Quick Reply Sets</div>
|
||||
<div class="qr--actions">
|
||||
<div class="qr--setListAdd menu_button menu_button_icon fa-solid fa-plus" id="qr--global-setListAdd" title="Add quick reply set"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="qr--global-setList" class="qr--setList"></div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div id="qr--chat">
|
||||
<div class="qr--head">
|
||||
<div class="qr--title">Chat Quick Reply Sets</div>
|
||||
<div class="qr--actions">
|
||||
<div class="qr--setListAdd menu_button menu_button_icon fa-solid fa-plus" id="qr--chat-setListAdd" title="Add quick reply set"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="qr--chat-setList" class="qr--setList"></div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div id="qr--editor">
|
||||
<div class="qr--head">
|
||||
<div class="qr--title">Edit Quick Replies</div>
|
||||
<div class="qr--actions">
|
||||
<select id="qr--set" class="text_pole"></select>
|
||||
<div class="qr--add menu_button menu_button_icon fa-solid fa-plus" id="qr--set-new" title="Create new quick reply set"></div>
|
||||
<div class="qr--add menu_button menu_button_icon fa-solid fa-file-import" id="qr--set-import" title="Import quick reply set"></div>
|
||||
<input type="file" id="qr--set-importFile" accept=".json" hidden>
|
||||
<div class="qr--add menu_button menu_button_icon fa-solid fa-file-export" id="qr--set-export" title="Export quick reply set"></div>
|
||||
<div class="qr--del menu_button menu_button_icon fa-solid fa-trash redWarningBG" id="qr--set-delete" title="Delete quick reply set"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="qr--set-settings">
|
||||
<label class="flex-container">
|
||||
<input type="checkbox" id="qr--disableSend"> <span>Disable send (insert into input field)</span>
|
||||
</label>
|
||||
<label class="flex-container">
|
||||
<input type="checkbox" id="qr--placeBeforeInput"> <span>Place quick reply before input</span>
|
||||
</label>
|
||||
<label class="flex-container" id="qr--injectInputContainer">
|
||||
<input type="checkbox" id="qr--injectInput"> <span>Inject user input automatically <small>(if disabled, use <code>{{input}}</code> macro for manual injection)</small></span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="qr--set-qrList" class="qr--qrList"></div>
|
||||
<div class="qr--set-qrListActions">
|
||||
<div class="qr--add menu_button menu_button_icon fa-solid fa-plus" id="qr--set-add" title="Add quick reply"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
File diff suppressed because it is too large
Load Diff
@ -6,6 +6,6 @@
|
||||
"js": "index.js",
|
||||
"css": "style.css",
|
||||
"author": "RossAscends#1779",
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.0",
|
||||
"homePage": "https://github.com/SillyTavern/SillyTavern"
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,76 @@
|
||||
import { warn } from '../index.js';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { QuickReply } from './QuickReply.js';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { QuickReplySettings } from './QuickReplySettings.js';
|
||||
|
||||
export class AutoExecuteHandler {
|
||||
/**@type {QuickReplySettings}*/ settings;
|
||||
|
||||
/**@type {Boolean[]}*/ preventAutoExecuteStack = [];
|
||||
|
||||
|
||||
|
||||
|
||||
constructor(/**@type {QuickReplySettings}*/settings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
|
||||
checkExecute() {
|
||||
return this.settings.isEnabled && !this.preventAutoExecuteStack.slice(-1)[0];
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
async performAutoExecute(/**@type {QuickReply[]}*/qrList) {
|
||||
for (const qr of qrList) {
|
||||
this.preventAutoExecuteStack.push(qr.preventAutoExecute);
|
||||
try {
|
||||
await qr.execute({ isAutoExecute:true });
|
||||
} catch (ex) {
|
||||
warn(ex);
|
||||
} finally {
|
||||
this.preventAutoExecuteStack.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async handleStartup() {
|
||||
if (!this.checkExecute()) return;
|
||||
const qrList = [
|
||||
...this.settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnStartup)).flat(),
|
||||
...(this.settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnStartup))?.flat() ?? []),
|
||||
];
|
||||
await this.performAutoExecute(qrList);
|
||||
}
|
||||
|
||||
async handleUser() {
|
||||
if (!this.checkExecute()) return;
|
||||
const qrList = [
|
||||
...this.settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnUser)).flat(),
|
||||
...(this.settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnUser))?.flat() ?? []),
|
||||
];
|
||||
await this.performAutoExecute(qrList);
|
||||
}
|
||||
|
||||
async handleAi() {
|
||||
if (!this.checkExecute()) return;
|
||||
const qrList = [
|
||||
...this.settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnAi)).flat(),
|
||||
...(this.settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnAi))?.flat() ?? []),
|
||||
];
|
||||
await this.performAutoExecute(qrList);
|
||||
}
|
||||
|
||||
async handleChatChanged() {
|
||||
if (!this.checkExecute()) return;
|
||||
const qrList = [
|
||||
...this.settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnChatChange)).flat(),
|
||||
...(this.settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnChatChange))?.flat() ?? []),
|
||||
];
|
||||
await this.performAutoExecute(qrList);
|
||||
}
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
/**
|
||||
* @typedef {import('./MenuItem.js').MenuItem} MenuItem
|
||||
*/
|
||||
|
||||
export class ContextMenu {
|
||||
/**@type {MenuItem[]}*/ itemList = [];
|
||||
/**@type {Boolean}*/ isActive = false;
|
||||
|
||||
/**@type {HTMLElement}*/ root;
|
||||
/**@type {HTMLElement}*/ menu;
|
||||
|
||||
|
||||
|
||||
|
||||
constructor(/**@type {MenuItem[]}*/items) {
|
||||
this.itemList = items;
|
||||
items.forEach(item => {
|
||||
item.onExpand = () => {
|
||||
items.filter(it => it != item)
|
||||
.forEach(it => it.collapse());
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.root) {
|
||||
const blocker = document.createElement('div'); {
|
||||
this.root = blocker;
|
||||
blocker.classList.add('ctx-blocker');
|
||||
blocker.addEventListener('click', () => this.hide());
|
||||
const menu = document.createElement('ul'); {
|
||||
this.menu = menu;
|
||||
menu.classList.add('list-group');
|
||||
menu.classList.add('ctx-menu');
|
||||
this.itemList.forEach(it => menu.append(it.render()));
|
||||
blocker.append(menu);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.root;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
show({ clientX, clientY }) {
|
||||
if (this.isActive) return;
|
||||
this.isActive = true;
|
||||
this.render();
|
||||
this.menu.style.bottom = `${window.innerHeight - clientY}px`;
|
||||
this.menu.style.left = `${clientX}px`;
|
||||
document.body.append(this.root);
|
||||
}
|
||||
hide() {
|
||||
if (this.root) {
|
||||
this.root.remove();
|
||||
}
|
||||
this.isActive = false;
|
||||
}
|
||||
toggle(/**@type {PointerEvent}*/evt) {
|
||||
if (this.isActive) {
|
||||
this.hide();
|
||||
} else {
|
||||
this.show(evt);
|
||||
}
|
||||
}
|
||||
}
|
489
public/scripts/extensions/quick-reply/src/QuickReply.js
Normal file
489
public/scripts/extensions/quick-reply/src/QuickReply.js
Normal file
@ -0,0 +1,489 @@
|
||||
import { callPopup } from '../../../../script.js';
|
||||
import { getSortableDelay } from '../../../utils.js';
|
||||
import { log, warn } from '../index.js';
|
||||
import { QuickReplyContextLink } from './QuickReplyContextLink.js';
|
||||
import { QuickReplySet } from './QuickReplySet.js';
|
||||
import { ContextMenu } from './ui/ctx/ContextMenu.js';
|
||||
|
||||
export class QuickReply {
|
||||
/**
|
||||
* @param {{ id?: number; contextList?: any; }} props
|
||||
*/
|
||||
static from(props) {
|
||||
props.contextList = (props.contextList ?? []).map((/** @type {any} */ it)=>QuickReplyContextLink.from(it));
|
||||
return Object.assign(new this(), props);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**@type {Number}*/ id;
|
||||
/**@type {String}*/ label = '';
|
||||
/**@type {String}*/ title = '';
|
||||
/**@type {String}*/ message = '';
|
||||
|
||||
/**@type {QuickReplyContextLink[]}*/ contextList;
|
||||
|
||||
/**@type {Boolean}*/ preventAutoExecute = true;
|
||||
/**@type {Boolean}*/ isHidden = false;
|
||||
/**@type {Boolean}*/ executeOnStartup = false;
|
||||
/**@type {Boolean}*/ executeOnUser = false;
|
||||
/**@type {Boolean}*/ executeOnAi = false;
|
||||
/**@type {Boolean}*/ executeOnChatChange = false;
|
||||
|
||||
/**@type {Function}*/ onExecute;
|
||||
/**@type {Function}*/ onDelete;
|
||||
/**@type {Function}*/ onUpdate;
|
||||
|
||||
|
||||
/**@type {HTMLElement}*/ dom;
|
||||
/**@type {HTMLElement}*/ domLabel;
|
||||
/**@type {HTMLElement}*/ settingsDom;
|
||||
/**@type {HTMLInputElement}*/ settingsDomLabel;
|
||||
/**@type {HTMLTextAreaElement}*/ settingsDomMessage;
|
||||
|
||||
|
||||
get hasContext() {
|
||||
return this.contextList && this.contextList.length > 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
unrender() {
|
||||
this.dom?.remove();
|
||||
this.dom = null;
|
||||
}
|
||||
updateRender() {
|
||||
if (!this.dom) return;
|
||||
this.dom.title = this.title || this.message;
|
||||
this.domLabel.textContent = this.label;
|
||||
this.dom.classList[this.hasContext ? 'add' : 'remove']('qr--hasCtx');
|
||||
}
|
||||
render() {
|
||||
this.unrender();
|
||||
if (!this.dom) {
|
||||
const root = document.createElement('div'); {
|
||||
this.dom = root;
|
||||
root.classList.add('qr--button');
|
||||
root.classList.add('menu_button');
|
||||
if (this.hasContext) {
|
||||
root.classList.add('qr--hasCtx');
|
||||
}
|
||||
root.title = this.title || this.message;
|
||||
root.addEventListener('contextmenu', (evt) => {
|
||||
log('contextmenu', this, this.hasContext);
|
||||
if (this.hasContext) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
const menu = new ContextMenu(this);
|
||||
menu.show(evt);
|
||||
}
|
||||
});
|
||||
root.addEventListener('click', (evt)=>{
|
||||
if (evt.ctrlKey) {
|
||||
this.showEditor();
|
||||
return;
|
||||
}
|
||||
this.execute();
|
||||
});
|
||||
const lbl = document.createElement('div'); {
|
||||
this.domLabel = lbl;
|
||||
lbl.classList.add('qr--button-label');
|
||||
lbl.textContent = this.label;
|
||||
root.append(lbl);
|
||||
}
|
||||
const expander = document.createElement('div'); {
|
||||
expander.classList.add('qr--button-expander');
|
||||
expander.textContent = '⋮';
|
||||
expander.title = 'Open context menu';
|
||||
expander.addEventListener('click', (evt) => {
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
const menu = new ContextMenu(this);
|
||||
menu.show(evt);
|
||||
});
|
||||
root.append(expander);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.dom;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
renderSettings(idx) {
|
||||
if (!this.settingsDom) {
|
||||
const item = document.createElement('div'); {
|
||||
this.settingsDom = item;
|
||||
item.classList.add('qr--set-item');
|
||||
item.setAttribute('data-order', String(idx));
|
||||
item.setAttribute('data-id', String(this.id));
|
||||
const drag = document.createElement('div'); {
|
||||
drag.classList.add('drag-handle');
|
||||
drag.classList.add('ui-sortable-handle');
|
||||
drag.textContent = '☰';
|
||||
item.append(drag);
|
||||
}
|
||||
const lblContainer = document.createElement('div'); {
|
||||
lblContainer.classList.add('qr--set-itemLabelContainer');
|
||||
const lbl = document.createElement('input'); {
|
||||
this.settingsDomLabel = lbl;
|
||||
lbl.classList.add('qr--set-itemLabel');
|
||||
lbl.classList.add('text_pole');
|
||||
lbl.value = this.label;
|
||||
lbl.addEventListener('input', ()=>this.updateLabel(lbl.value));
|
||||
lblContainer.append(lbl);
|
||||
}
|
||||
item.append(lblContainer);
|
||||
}
|
||||
const optContainer = document.createElement('div'); {
|
||||
optContainer.classList.add('qr--set-optionsContainer');
|
||||
const opt = document.createElement('div'); {
|
||||
opt.classList.add('qr--action');
|
||||
opt.classList.add('menu_button');
|
||||
opt.classList.add('fa-solid');
|
||||
opt.textContent = '⁝';
|
||||
opt.title = 'Additional options:\n - large editor\n - context menu\n - auto-execution\n - tooltip';
|
||||
opt.addEventListener('click', ()=>this.showEditor());
|
||||
optContainer.append(opt);
|
||||
}
|
||||
item.append(optContainer);
|
||||
}
|
||||
const mes = document.createElement('textarea'); {
|
||||
this.settingsDomMessage = mes;
|
||||
mes.id = `qr--set--item${this.id}`;
|
||||
mes.classList.add('qr--set-itemMessage');
|
||||
mes.value = this.message;
|
||||
//HACK need to use jQuery to catch the triggered event from the expanded editor
|
||||
$(mes).on('input', ()=>this.updateMessage(mes.value));
|
||||
item.append(mes);
|
||||
}
|
||||
const actions = document.createElement('div'); {
|
||||
actions.classList.add('qr--actions');
|
||||
const del = document.createElement('div'); {
|
||||
del.classList.add('qr--action');
|
||||
del.classList.add('menu_button');
|
||||
del.classList.add('menu_button_icon');
|
||||
del.classList.add('fa-solid');
|
||||
del.classList.add('fa-trash-can');
|
||||
del.classList.add('redWarningBG');
|
||||
del.title = 'Remove quick reply';
|
||||
del.addEventListener('click', ()=>this.delete());
|
||||
actions.append(del);
|
||||
}
|
||||
item.append(actions);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.settingsDom;
|
||||
}
|
||||
unrenderSettings() {
|
||||
this.settingsDom?.remove();
|
||||
}
|
||||
|
||||
async showEditor() {
|
||||
const response = await fetch('/scripts/extensions/quick-reply/html/qrEditor.html', { cache: 'no-store' });
|
||||
if (response.ok) {
|
||||
this.template = document.createRange().createContextualFragment(await response.text()).querySelector('#qr--modalEditor');
|
||||
/**@type {HTMLElement} */
|
||||
// @ts-ignore
|
||||
const dom = this.template.cloneNode(true);
|
||||
const popupResult = callPopup(dom, 'text', undefined, { okButton: 'OK', wide: true, large: true, rows: 1 });
|
||||
|
||||
// basics
|
||||
/**@type {HTMLInputElement}*/
|
||||
const label = dom.querySelector('#qr--modal-label');
|
||||
label.value = this.label;
|
||||
label.addEventListener('input', ()=>{
|
||||
this.updateLabel(label.value);
|
||||
});
|
||||
/**@type {HTMLInputElement}*/
|
||||
const title = dom.querySelector('#qr--modal-title');
|
||||
title.value = this.title;
|
||||
title.addEventListener('input', () => {
|
||||
this.updateTitle(title.value);
|
||||
});
|
||||
/**@type {HTMLTextAreaElement}*/
|
||||
const message = dom.querySelector('#qr--modal-message');
|
||||
message.value = this.message;
|
||||
message.addEventListener('input', () => {
|
||||
this.updateMessage(message.value);
|
||||
});
|
||||
//TODO move tab support for textarea into its own helper(?) and use for both this and .editor_maximize
|
||||
message.addEventListener('keydown', (evt) => {
|
||||
if (evt.key == 'Tab' && !evt.shiftKey && !evt.ctrlKey && !evt.altKey) {
|
||||
evt.preventDefault();
|
||||
const start = message.selectionStart;
|
||||
const end = message.selectionEnd;
|
||||
if (end - start > 0 && message.value.substring(start, end).includes('\n')) {
|
||||
const lineStart = message.value.lastIndexOf('\n', start);
|
||||
const count = message.value.substring(lineStart, end).split('\n').length - 1;
|
||||
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);
|
||||
} 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);
|
||||
}
|
||||
} else if (evt.key == 'Tab' && evt.shiftKey && !evt.ctrlKey && !evt.altKey) {
|
||||
evt.preventDefault();
|
||||
const start = message.selectionStart;
|
||||
const end = message.selectionEnd;
|
||||
const lineStart = message.value.lastIndexOf('\n', start);
|
||||
const count = message.value.substring(lineStart, end).split('\n\t').length - 1;
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
// context menu
|
||||
/**@type {HTMLTemplateElement}*/
|
||||
const tpl = dom.querySelector('#qr--ctxItem');
|
||||
const linkList = dom.querySelector('#qr--ctxEditor');
|
||||
const fillQrSetSelect = (/**@type {HTMLSelectElement}*/select, /**@type {QuickReplyContextLink}*/ link) => {
|
||||
[{ name: 'Select a QR set' }, ...QuickReplySet.list].forEach(qrs => {
|
||||
const opt = document.createElement('option'); {
|
||||
opt.value = qrs.name;
|
||||
opt.textContent = qrs.name;
|
||||
opt.selected = qrs.name == link.set?.name;
|
||||
select.append(opt);
|
||||
}
|
||||
});
|
||||
};
|
||||
const addCtxItem = (/**@type {QuickReplyContextLink}*/link, /**@type {Number}*/idx) => {
|
||||
/**@type {HTMLElement} */
|
||||
// @ts-ignore
|
||||
const itemDom = tpl.content.querySelector('.qr--ctxItem').cloneNode(true); {
|
||||
itemDom.setAttribute('data-order', String(idx));
|
||||
|
||||
/**@type {HTMLSelectElement} */
|
||||
const select = itemDom.querySelector('.qr--set');
|
||||
fillQrSetSelect(select, link);
|
||||
select.addEventListener('change', () => {
|
||||
link.set = QuickReplySet.get(select.value);
|
||||
this.updateContext();
|
||||
});
|
||||
|
||||
/**@type {HTMLInputElement} */
|
||||
const chain = itemDom.querySelector('.qr--isChained');
|
||||
chain.checked = link.isChained;
|
||||
chain.addEventListener('click', () => {
|
||||
link.isChained = chain.checked;
|
||||
this.updateContext();
|
||||
});
|
||||
|
||||
itemDom.querySelector('.qr--delete').addEventListener('click', () => {
|
||||
itemDom.remove();
|
||||
this.contextList.splice(this.contextList.indexOf(link), 1);
|
||||
this.updateContext();
|
||||
});
|
||||
|
||||
linkList.append(itemDom);
|
||||
}
|
||||
};
|
||||
[...this.contextList].forEach((link, idx) => addCtxItem(link, idx));
|
||||
dom.querySelector('#qr--ctxAdd').addEventListener('click', () => {
|
||||
const link = new QuickReplyContextLink();
|
||||
this.contextList.push(link);
|
||||
addCtxItem(link, this.contextList.length - 1);
|
||||
});
|
||||
const onContextSort = () => {
|
||||
this.contextList = Array.from(linkList.querySelectorAll('.qr--ctxItem')).map((it,idx) => {
|
||||
const link = this.contextList[Number(it.getAttribute('data-order'))];
|
||||
it.setAttribute('data-order', String(idx));
|
||||
return link;
|
||||
});
|
||||
this.updateContext();
|
||||
};
|
||||
// @ts-ignore
|
||||
$(linkList).sortable({
|
||||
delay: getSortableDelay(),
|
||||
stop: () => onContextSort(),
|
||||
});
|
||||
|
||||
// auto-exec
|
||||
/**@type {HTMLInputElement}*/
|
||||
const preventAutoExecute = dom.querySelector('#qr--preventAutoExecute');
|
||||
preventAutoExecute.checked = this.preventAutoExecute;
|
||||
preventAutoExecute.addEventListener('click', ()=>{
|
||||
this.preventAutoExecute = preventAutoExecute.checked;
|
||||
this.updateContext();
|
||||
});
|
||||
/**@type {HTMLInputElement}*/
|
||||
const isHidden = dom.querySelector('#qr--isHidden');
|
||||
isHidden.checked = this.isHidden;
|
||||
isHidden.addEventListener('click', ()=>{
|
||||
this.isHidden = isHidden.checked;
|
||||
this.updateContext();
|
||||
});
|
||||
/**@type {HTMLInputElement}*/
|
||||
const executeOnStartup = dom.querySelector('#qr--executeOnStartup');
|
||||
executeOnStartup.checked = this.executeOnStartup;
|
||||
executeOnStartup.addEventListener('click', ()=>{
|
||||
this.executeOnStartup = executeOnStartup.checked;
|
||||
this.updateContext();
|
||||
});
|
||||
/**@type {HTMLInputElement}*/
|
||||
const executeOnUser = dom.querySelector('#qr--executeOnUser');
|
||||
executeOnUser.checked = this.executeOnUser;
|
||||
executeOnUser.addEventListener('click', ()=>{
|
||||
this.executeOnUser = executeOnUser.checked;
|
||||
this.updateContext();
|
||||
});
|
||||
/**@type {HTMLInputElement}*/
|
||||
const executeOnAi = dom.querySelector('#qr--executeOnAi');
|
||||
executeOnAi.checked = this.executeOnAi;
|
||||
executeOnAi.addEventListener('click', ()=>{
|
||||
this.executeOnAi = executeOnAi.checked;
|
||||
this.updateContext();
|
||||
});
|
||||
/**@type {HTMLInputElement}*/
|
||||
const executeOnChatChange = dom.querySelector('#qr--executeOnChatChange');
|
||||
executeOnChatChange.checked = this.executeOnChatChange;
|
||||
executeOnChatChange.addEventListener('click', ()=>{
|
||||
this.executeOnChatChange = executeOnChatChange.checked;
|
||||
this.updateContext();
|
||||
});
|
||||
|
||||
|
||||
/**@type {HTMLElement}*/
|
||||
const executeErrors = dom.querySelector('#qr--modal-executeErrors');
|
||||
/**@type {HTMLInputElement}*/
|
||||
const executeHide = dom.querySelector('#qr--modal-executeHide');
|
||||
let executePromise;
|
||||
/**@type {HTMLElement}*/
|
||||
const executeBtn = dom.querySelector('#qr--modal-execute');
|
||||
executeBtn.addEventListener('click', async()=>{
|
||||
if (executePromise) return;
|
||||
executeBtn.classList.add('qr--busy');
|
||||
executeErrors.innerHTML = '';
|
||||
if (executeHide.checked) {
|
||||
document.querySelector('#shadow_popup').classList.add('qr--hide');
|
||||
}
|
||||
try {
|
||||
executePromise = this.execute();
|
||||
await executePromise;
|
||||
} catch (ex) {
|
||||
executeErrors.textContent = ex.message;
|
||||
}
|
||||
executePromise = null;
|
||||
executeBtn.classList.remove('qr--busy');
|
||||
document.querySelector('#shadow_popup').classList.remove('qr--hide');
|
||||
});
|
||||
|
||||
await popupResult;
|
||||
} else {
|
||||
warn('failed to fetch qrEditor template');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
delete() {
|
||||
if (this.onDelete) {
|
||||
this.unrender();
|
||||
this.unrenderSettings();
|
||||
this.onDelete(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
*/
|
||||
updateMessage(value) {
|
||||
if (this.onUpdate) {
|
||||
if (this.settingsDomMessage && this.settingsDomMessage.value != value) {
|
||||
this.settingsDomMessage.value = value;
|
||||
}
|
||||
this.message = value;
|
||||
this.updateRender();
|
||||
this.onUpdate(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
*/
|
||||
updateLabel(value) {
|
||||
if (this.onUpdate) {
|
||||
if (this.settingsDomLabel && this.settingsDomLabel.value != value) {
|
||||
this.settingsDomLabel.value = value;
|
||||
}
|
||||
this.label = value;
|
||||
this.updateRender();
|
||||
this.onUpdate(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
*/
|
||||
updateTitle(value) {
|
||||
if (this.onUpdate) {
|
||||
this.title = value;
|
||||
this.updateRender();
|
||||
this.onUpdate(this);
|
||||
}
|
||||
}
|
||||
|
||||
updateContext() {
|
||||
if (this.onUpdate) {
|
||||
this.updateRender();
|
||||
this.onUpdate(this);
|
||||
}
|
||||
}
|
||||
addContextLink(cl) {
|
||||
this.contextList.push(cl);
|
||||
this.updateContext();
|
||||
}
|
||||
removeContextLink(setName) {
|
||||
const idx = this.contextList.findIndex(it=>it.set.name == setName);
|
||||
if (idx > -1) {
|
||||
this.contextList.splice(idx, 1);
|
||||
this.updateContext();
|
||||
}
|
||||
}
|
||||
clearContextLinks() {
|
||||
if (this.contextList.length) {
|
||||
this.contextList.splice(0, this.contextList.length);
|
||||
this.updateContext();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async execute(args = {}) {
|
||||
if (this.message?.length > 0 && this.onExecute) {
|
||||
const message = this.message.replace(/\{\{arg::([^}]+)\}\}/g, (_, key) => {
|
||||
return args[key] ?? '';
|
||||
});
|
||||
return await this.onExecute(this, message, args.isAutoExecute ?? false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
label: this.label,
|
||||
title: this.title,
|
||||
message: this.message,
|
||||
contextList: this.contextList,
|
||||
preventAutoExecute: this.preventAutoExecute,
|
||||
isHidden: this.isHidden,
|
||||
executeOnStartup: this.executeOnStartup,
|
||||
executeOnUser: this.executeOnUser,
|
||||
executeOnAi: this.executeOnAi,
|
||||
executeOnChatChange: this.executeOnChatChange,
|
||||
};
|
||||
}
|
||||
}
|
122
public/scripts/extensions/quick-reply/src/QuickReplyConfig.js
Normal file
122
public/scripts/extensions/quick-reply/src/QuickReplyConfig.js
Normal file
@ -0,0 +1,122 @@
|
||||
import { getSortableDelay } from '../../../utils.js';
|
||||
import { QuickReplySetLink } from './QuickReplySetLink.js';
|
||||
import { QuickReplySet } from './QuickReplySet.js';
|
||||
|
||||
export class QuickReplyConfig {
|
||||
/**@type {QuickReplySetLink[]}*/ setList = [];
|
||||
/**@type {Boolean}*/ isGlobal;
|
||||
|
||||
/**@type {Function}*/ onUpdate;
|
||||
/**@type {Function}*/ onRequestEditSet;
|
||||
|
||||
/**@type {HTMLElement}*/ dom;
|
||||
/**@type {HTMLElement}*/ setListDom;
|
||||
|
||||
|
||||
|
||||
|
||||
static from(props) {
|
||||
props.setList = props.setList?.map(it=>QuickReplySetLink.from(it))?.filter(it=>it.set) ?? [];
|
||||
const instance = Object.assign(new this(), props);
|
||||
instance.init();
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
init() {
|
||||
this.setList.forEach(it=>this.hookQuickReplyLink(it));
|
||||
}
|
||||
|
||||
|
||||
hasSet(qrs) {
|
||||
return this.setList.find(it=>it.set == qrs) != null;
|
||||
}
|
||||
addSet(qrs, isVisible = true) {
|
||||
if (!this.hasSet(qrs)) {
|
||||
const qrl = new QuickReplySetLink();
|
||||
qrl.set = qrs;
|
||||
qrl.isVisible = isVisible;
|
||||
this.hookQuickReplyLink(qrl);
|
||||
this.setList.push(qrl);
|
||||
this.setListDom.append(qrl.renderSettings(this.setList.length - 1));
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
removeSet(qrs) {
|
||||
const idx = this.setList.findIndex(it=>it.set == qrs);
|
||||
if (idx > -1) {
|
||||
this.setList.splice(idx, 1);
|
||||
this.update();
|
||||
this.updateSetListDom();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
renderSettingsInto(/**@type {HTMLElement}*/root) {
|
||||
/**@type {HTMLElement}*/
|
||||
this.setListDom = root.querySelector('.qr--setList');
|
||||
root.querySelector('.qr--setListAdd').addEventListener('click', ()=>{
|
||||
this.addSet(QuickReplySet.list[0]);
|
||||
});
|
||||
this.updateSetListDom();
|
||||
}
|
||||
updateSetListDom() {
|
||||
this.setListDom.innerHTML = '';
|
||||
// @ts-ignore
|
||||
$(this.setListDom).sortable({
|
||||
delay: getSortableDelay(),
|
||||
stop: ()=>this.onSetListSort(),
|
||||
});
|
||||
this.setList.filter(it=>!it.set.isDeleted).forEach((qrl,idx)=>this.setListDom.append(qrl.renderSettings(idx)));
|
||||
}
|
||||
|
||||
|
||||
onSetListSort() {
|
||||
this.setList = Array.from(this.setListDom.children).map((it,idx)=>{
|
||||
const qrl = this.setList[Number(it.getAttribute('data-order'))];
|
||||
qrl.index = idx;
|
||||
it.setAttribute('data-order', String(idx));
|
||||
return qrl;
|
||||
});
|
||||
this.update();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @param {QuickReplySetLink} qrl
|
||||
*/
|
||||
hookQuickReplyLink(qrl) {
|
||||
qrl.onDelete = ()=>this.deleteQuickReplyLink(qrl);
|
||||
qrl.onUpdate = ()=>this.update();
|
||||
qrl.onRequestEditSet = ()=>this.requestEditSet(qrl.set);
|
||||
}
|
||||
|
||||
deleteQuickReplyLink(qrl) {
|
||||
this.setList.splice(this.setList.indexOf(qrl), 1);
|
||||
this.update();
|
||||
}
|
||||
|
||||
update() {
|
||||
if (this.onUpdate) {
|
||||
this.onUpdate(this);
|
||||
}
|
||||
}
|
||||
|
||||
requestEditSet(qrs) {
|
||||
if (this.onRequestEditSet) {
|
||||
this.onRequestEditSet(qrs);
|
||||
}
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
setList: this.setList,
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
import { QuickReplySet } from './QuickReplySet.js';
|
||||
|
||||
export class QuickReplyContextLink {
|
||||
static from(props) {
|
||||
props.set = QuickReplySet.get(props.set);
|
||||
const x = Object.assign(new this(), props);
|
||||
return x;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**@type {QuickReplySet}*/ set;
|
||||
/**@type {Boolean}*/ isChained = false;
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
set: this.set?.name,
|
||||
isChained: this.isChained,
|
||||
};
|
||||
}
|
||||
}
|
209
public/scripts/extensions/quick-reply/src/QuickReplySet.js
Normal file
209
public/scripts/extensions/quick-reply/src/QuickReplySet.js
Normal file
@ -0,0 +1,209 @@
|
||||
import { getRequestHeaders, substituteParams } from '../../../../script.js';
|
||||
import { executeSlashCommands } from '../../../slash-commands.js';
|
||||
import { debounceAsync, warn } from '../index.js';
|
||||
import { QuickReply } from './QuickReply.js';
|
||||
|
||||
export class QuickReplySet {
|
||||
/**@type {QuickReplySet[]}*/ static list = [];
|
||||
|
||||
|
||||
static from(props) {
|
||||
props.qrList = []; //props.qrList?.map(it=>QuickReply.from(it));
|
||||
const instance = Object.assign(new this(), props);
|
||||
// instance.init();
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} name - name of the QuickReplySet
|
||||
*/
|
||||
static get(name) {
|
||||
return this.list.find(it=>it.name == name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**@type {String}*/ name;
|
||||
/**@type {Boolean}*/ disableSend = false;
|
||||
/**@type {Boolean}*/ placeBeforeInput = false;
|
||||
/**@type {Boolean}*/ injectInput = false;
|
||||
/**@type {QuickReply[]}*/ qrList = [];
|
||||
|
||||
/**@type {Number}*/ idIndex = 0;
|
||||
|
||||
/**@type {Boolean}*/ isDeleted = false;
|
||||
|
||||
/**@type {Function}*/ save;
|
||||
|
||||
/**@type {HTMLElement}*/ dom;
|
||||
/**@type {HTMLElement}*/ settingsDom;
|
||||
|
||||
|
||||
|
||||
|
||||
constructor() {
|
||||
this.save = debounceAsync(()=>this.performSave(), 200);
|
||||
}
|
||||
|
||||
init() {
|
||||
this.qrList.forEach(qr=>this.hookQuickReply(qr));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
unrender() {
|
||||
this.dom?.remove();
|
||||
this.dom = null;
|
||||
}
|
||||
render() {
|
||||
this.unrender();
|
||||
if (!this.dom) {
|
||||
const root = document.createElement('div'); {
|
||||
this.dom = root;
|
||||
root.classList.add('qr--buttons');
|
||||
this.qrList.filter(qr=>!qr.isHidden).forEach(qr=>{
|
||||
root.append(qr.render());
|
||||
});
|
||||
}
|
||||
}
|
||||
return this.dom;
|
||||
}
|
||||
rerender() {
|
||||
if (!this.dom) return;
|
||||
this.dom.innerHTML = '';
|
||||
this.qrList.filter(qr=>!qr.isHidden).forEach(qr=>{
|
||||
this.dom.append(qr.render());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
renderSettings() {
|
||||
if (!this.settingsDom) {
|
||||
this.settingsDom = document.createElement('div'); {
|
||||
this.settingsDom.classList.add('qr--set-qrListContents');
|
||||
this.qrList.forEach((qr,idx)=>{
|
||||
this.renderSettingsItem(qr, idx);
|
||||
});
|
||||
}
|
||||
}
|
||||
return this.settingsDom;
|
||||
}
|
||||
renderSettingsItem(qr, idx) {
|
||||
this.settingsDom.append(qr.renderSettings(idx));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @param {QuickReply} qr
|
||||
* @param {String} [message] - optional altered message to be used
|
||||
*/
|
||||
async execute(qr, message = null, isAutoExecute = false) {
|
||||
/**@type {HTMLTextAreaElement}*/
|
||||
const ta = document.querySelector('#send_textarea');
|
||||
const finalMessage = message ?? qr.message;
|
||||
let input = ta.value;
|
||||
if (!isAutoExecute && this.injectInput && input.length > 0) {
|
||||
if (this.placeBeforeInput) {
|
||||
input = `${finalMessage} ${input}`;
|
||||
} else {
|
||||
input = `${input} ${finalMessage}`;
|
||||
}
|
||||
} else {
|
||||
input = `${finalMessage} `;
|
||||
}
|
||||
|
||||
if (input[0] == '/' && !this.disableSend) {
|
||||
const result = await executeSlashCommands(input);
|
||||
return typeof result === 'object' ? result?.pipe : '';
|
||||
}
|
||||
|
||||
ta.value = substituteParams(input);
|
||||
ta.focus();
|
||||
|
||||
if (!this.disableSend) {
|
||||
// @ts-ignore
|
||||
document.querySelector('#send_but').click();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
addQuickReply() {
|
||||
const id = Math.max(this.idIndex, this.qrList.reduce((max,qr)=>Math.max(max,qr.id),0)) + 1;
|
||||
this.idIndex = id + 1;
|
||||
const qr = QuickReply.from({ id });
|
||||
this.qrList.push(qr);
|
||||
this.hookQuickReply(qr);
|
||||
if (this.settingsDom) {
|
||||
this.renderSettingsItem(qr, this.qrList.length - 1);
|
||||
}
|
||||
if (this.dom) {
|
||||
this.dom.append(qr.render());
|
||||
}
|
||||
this.save();
|
||||
return qr;
|
||||
}
|
||||
|
||||
hookQuickReply(qr) {
|
||||
qr.onExecute = (_, message, isAutoExecute)=>this.execute(qr, message, isAutoExecute);
|
||||
qr.onDelete = ()=>this.removeQuickReply(qr);
|
||||
qr.onUpdate = ()=>this.save();
|
||||
}
|
||||
|
||||
removeQuickReply(qr) {
|
||||
this.qrList.splice(this.qrList.indexOf(qr), 1);
|
||||
this.save();
|
||||
}
|
||||
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
version: 2,
|
||||
name: this.name,
|
||||
disableSend: this.disableSend,
|
||||
placeBeforeInput: this.placeBeforeInput,
|
||||
injectInput: this.injectInput,
|
||||
qrList: this.qrList,
|
||||
idIndex: this.idIndex,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
async performSave() {
|
||||
const response = await fetch('/savequickreply', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify(this),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
this.rerender();
|
||||
} else {
|
||||
warn(`Failed to save Quick Reply Set: ${this.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
async delete() {
|
||||
const response = await fetch('/deletequickreply', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify(this),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
this.unrender();
|
||||
const idx = QuickReplySet.list.indexOf(this);
|
||||
QuickReplySet.list.splice(idx, 1);
|
||||
this.isDeleted = true;
|
||||
} else {
|
||||
warn(`Failed to delete Quick Reply Set: ${this.name}`);
|
||||
}
|
||||
}
|
||||
}
|
129
public/scripts/extensions/quick-reply/src/QuickReplySetLink.js
Normal file
129
public/scripts/extensions/quick-reply/src/QuickReplySetLink.js
Normal file
@ -0,0 +1,129 @@
|
||||
import { QuickReplySet } from './QuickReplySet.js';
|
||||
|
||||
export class QuickReplySetLink {
|
||||
static from(props) {
|
||||
props.set = QuickReplySet.get(props.set);
|
||||
/**@type {QuickReplySetLink}*/
|
||||
const instance = Object.assign(new this(), props);
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**@type {QuickReplySet}*/ set;
|
||||
/**@type {Boolean}*/ isVisible = true;
|
||||
|
||||
/**@type {Number}*/ index;
|
||||
|
||||
/**@type {Function}*/ onUpdate;
|
||||
/**@type {Function}*/ onRequestEditSet;
|
||||
/**@type {Function}*/ onDelete;
|
||||
|
||||
/**@type {HTMLElement}*/ settingsDom;
|
||||
|
||||
|
||||
|
||||
|
||||
renderSettings(idx) {
|
||||
this.index = idx;
|
||||
const item = document.createElement('div'); {
|
||||
this.settingsDom = item;
|
||||
item.classList.add('qr--item');
|
||||
item.setAttribute('data-order', String(this.index));
|
||||
const drag = document.createElement('div'); {
|
||||
drag.classList.add('drag-handle');
|
||||
drag.classList.add('ui-sortable-handle');
|
||||
drag.textContent = '☰';
|
||||
item.append(drag);
|
||||
}
|
||||
const set = document.createElement('select'); {
|
||||
set.classList.add('qr--set');
|
||||
// fix for jQuery sortable breaking childrens' touch events
|
||||
set.addEventListener('touchstart', (evt)=>evt.stopPropagation());
|
||||
set.addEventListener('change', ()=>{
|
||||
this.set = QuickReplySet.get(set.value);
|
||||
this.update();
|
||||
});
|
||||
QuickReplySet.list.forEach(qrs=>{
|
||||
const opt = document.createElement('option'); {
|
||||
opt.value = qrs.name;
|
||||
opt.textContent = qrs.name;
|
||||
opt.selected = qrs == this.set;
|
||||
set.append(opt);
|
||||
}
|
||||
});
|
||||
item.append(set);
|
||||
}
|
||||
const visible = document.createElement('label'); {
|
||||
visible.classList.add('qr--visible');
|
||||
visible.title = 'Show buttons';
|
||||
const cb = document.createElement('input'); {
|
||||
cb.type = 'checkbox';
|
||||
cb.checked = this.isVisible;
|
||||
cb.addEventListener('click', ()=>{
|
||||
this.isVisible = cb.checked;
|
||||
this.update();
|
||||
});
|
||||
visible.append(cb);
|
||||
}
|
||||
visible.append('Buttons');
|
||||
item.append(visible);
|
||||
}
|
||||
const edit = document.createElement('div'); {
|
||||
edit.classList.add('menu_button');
|
||||
edit.classList.add('menu_button_icon');
|
||||
edit.classList.add('fa-solid');
|
||||
edit.classList.add('fa-pencil');
|
||||
edit.title = 'Edit quick reply set';
|
||||
edit.addEventListener('click', ()=>this.requestEditSet());
|
||||
item.append(edit);
|
||||
}
|
||||
const del = document.createElement('div'); {
|
||||
del.classList.add('qr--del');
|
||||
del.classList.add('menu_button');
|
||||
del.classList.add('menu_button_icon');
|
||||
del.classList.add('fa-solid');
|
||||
del.classList.add('fa-trash-can');
|
||||
del.title = 'Remove quick reply set';
|
||||
del.addEventListener('click', ()=>this.delete());
|
||||
item.append(del);
|
||||
}
|
||||
}
|
||||
return this.settingsDom;
|
||||
}
|
||||
unrenderSettings() {
|
||||
this.settingsDom?.remove();
|
||||
this.settingsDom = null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
update() {
|
||||
if (this.onUpdate) {
|
||||
this.onUpdate(this);
|
||||
}
|
||||
}
|
||||
requestEditSet() {
|
||||
if (this.onRequestEditSet) {
|
||||
this.onRequestEditSet(this.set);
|
||||
}
|
||||
}
|
||||
delete() {
|
||||
this.unrenderSettings();
|
||||
if (this.onDelete) {
|
||||
this.onDelete();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
set: this.set.name,
|
||||
isVisible: this.isVisible,
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
import { chat_metadata, saveChatDebounced, saveSettingsDebounced } from '../../../../script.js';
|
||||
import { extension_settings } from '../../../extensions.js';
|
||||
import { QuickReplyConfig } from './QuickReplyConfig.js';
|
||||
|
||||
export class QuickReplySettings {
|
||||
static from(props) {
|
||||
props.config = QuickReplyConfig.from(props.config);
|
||||
const instance = Object.assign(new this(), props);
|
||||
instance.init();
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**@type {Boolean}*/ isEnabled = false;
|
||||
/**@type {Boolean}*/ isCombined = false;
|
||||
/**@type {Boolean}*/ isPopout = false;
|
||||
/**@type {QuickReplyConfig}*/ config;
|
||||
/**@type {QuickReplyConfig}*/ _chatConfig;
|
||||
get chatConfig() {
|
||||
return this._chatConfig;
|
||||
}
|
||||
set chatConfig(value) {
|
||||
if (this._chatConfig != value) {
|
||||
this.unhookConfig(this._chatConfig);
|
||||
this._chatConfig = value;
|
||||
this.hookConfig(this._chatConfig);
|
||||
}
|
||||
}
|
||||
|
||||
/**@type {Function}*/ onSave;
|
||||
/**@type {Function}*/ onRequestEditSet;
|
||||
|
||||
|
||||
|
||||
|
||||
init() {
|
||||
this.hookConfig(this.config);
|
||||
this.hookConfig(this.chatConfig);
|
||||
}
|
||||
|
||||
hookConfig(config) {
|
||||
if (config) {
|
||||
config.onUpdate = ()=>this.save();
|
||||
config.onRequestEditSet = (qrs)=>this.requestEditSet(qrs);
|
||||
}
|
||||
}
|
||||
unhookConfig(config) {
|
||||
if (config) {
|
||||
config.onUpdate = null;
|
||||
config.onRequestEditSet = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
save() {
|
||||
extension_settings.quickReplyV2 = this.toJSON();
|
||||
saveSettingsDebounced();
|
||||
if (this.chatConfig) {
|
||||
chat_metadata.quickReply = this.chatConfig.toJSON();
|
||||
saveChatDebounced();
|
||||
}
|
||||
if (this.onSave) {
|
||||
this.onSave();
|
||||
}
|
||||
}
|
||||
|
||||
requestEditSet(qrs) {
|
||||
if (this.onRequestEditSet) {
|
||||
this.onRequestEditSet(qrs);
|
||||
}
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
isEnabled: this.isEnabled,
|
||||
isCombined: this.isCombined,
|
||||
isPopout: this.isPopout,
|
||||
config: this.config,
|
||||
};
|
||||
}
|
||||
}
|
270
public/scripts/extensions/quick-reply/src/SlashCommandHandler.js
Normal file
270
public/scripts/extensions/quick-reply/src/SlashCommandHandler.js
Normal file
@ -0,0 +1,270 @@
|
||||
import { registerSlashCommand } from '../../../slash-commands.js';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { QuickReplyApi } from '../api/QuickReplyApi.js';
|
||||
|
||||
export class SlashCommandHandler {
|
||||
/**@type {QuickReplyApi}*/ api;
|
||||
|
||||
|
||||
|
||||
|
||||
constructor(/**@type {QuickReplyApi}*/api) {
|
||||
this.api = api;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
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);
|
||||
|
||||
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
|
||||
title - bool - 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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
getSetByName(name) {
|
||||
const set = this.api.getSetByName(name);
|
||||
if (!set) {
|
||||
toastr.error(`No Quick Reply Set with the name "${name}" could be found.`);
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
getQrByLabel(setName, label) {
|
||||
const qr = this.api.getQrByLabel(setName, label);
|
||||
if (!qr) {
|
||||
toastr.error(`No Quick Reply with the label "${label}" could be found in the set "${setName}"`);
|
||||
}
|
||||
return qr;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
async executeQuickReplyByIndex(idx) {
|
||||
try {
|
||||
return await this.api.executeQuickReplyByIndex(idx);
|
||||
} catch (ex) {
|
||||
toastr.error(ex.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
toggleGlobalSet(name, args = {}) {
|
||||
try {
|
||||
this.api.toggleGlobalSet(name, JSON.parse(args.visible ?? 'true') === true);
|
||||
} catch (ex) {
|
||||
toastr.error(ex.message);
|
||||
}
|
||||
}
|
||||
addGlobalSet(name, args = {}) {
|
||||
try {
|
||||
this.api.addGlobalSet(name, JSON.parse(args.visible ?? 'true') === true);
|
||||
} catch (ex) {
|
||||
toastr.error(ex.message);
|
||||
}
|
||||
}
|
||||
removeGlobalSet(name) {
|
||||
try {
|
||||
this.api.removeGlobalSet(name);
|
||||
} catch (ex) {
|
||||
toastr.error(ex.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
toggleChatSet(name, args = {}) {
|
||||
try {
|
||||
this.api.toggleChatSet(name, JSON.parse(args.visible ?? 'true') === true);
|
||||
} catch (ex) {
|
||||
toastr.error(ex.message);
|
||||
}
|
||||
}
|
||||
addChatSet(name, args = {}) {
|
||||
try {
|
||||
this.api.addChatSet(name, JSON.parse(args.visible ?? 'true') === true);
|
||||
} catch (ex) {
|
||||
toastr.error(ex.message);
|
||||
}
|
||||
}
|
||||
removeChatSet(name) {
|
||||
try {
|
||||
this.api.removeChatSet(name);
|
||||
} catch (ex) {
|
||||
toastr.error(ex.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
createQuickReply(args, message) {
|
||||
try {
|
||||
this.api.createQuickReply(
|
||||
args.set ?? '',
|
||||
args.label ?? '',
|
||||
{
|
||||
message: message ?? '',
|
||||
title: args.title,
|
||||
isHidden: JSON.parse(args.hidden ?? 'false') === true,
|
||||
executeOnStartup: JSON.parse(args.startup ?? 'false') === true,
|
||||
executeOnUser: JSON.parse(args.user ?? 'false') === true,
|
||||
executeOnAi: JSON.parse(args.bot ?? 'false') === true,
|
||||
executeOnChatChange: JSON.parse(args.load ?? 'false') === true,
|
||||
},
|
||||
);
|
||||
} catch (ex) {
|
||||
toastr.error(ex.message);
|
||||
}
|
||||
}
|
||||
updateQuickReply(args, message) {
|
||||
try {
|
||||
this.api.updateQuickReply(
|
||||
args.set ?? '',
|
||||
args.label ?? '',
|
||||
{
|
||||
newLabel: args.newlabel,
|
||||
message: (message ?? '').trim().length > 0 ? message : undefined,
|
||||
title: args.title,
|
||||
isHidden: args.hidden,
|
||||
executeOnStartup: args.startup,
|
||||
executeOnUser: args.user,
|
||||
executeOnAi: args.bot,
|
||||
executeOnChatChange: args.load,
|
||||
},
|
||||
);
|
||||
} catch (ex) {
|
||||
toastr.error(ex.message);
|
||||
}
|
||||
}
|
||||
deleteQuickReply(args, label) {
|
||||
try {
|
||||
this.api.deleteQuickReply(args.set, label);
|
||||
} catch (ex) {
|
||||
toastr.error(ex.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
createContextItem(args, name) {
|
||||
try {
|
||||
this.api.createContextItem(
|
||||
args.set,
|
||||
args.label,
|
||||
name,
|
||||
JSON.parse(args.chain ?? 'false') === true,
|
||||
);
|
||||
} catch (ex) {
|
||||
toastr.error(ex.message);
|
||||
}
|
||||
}
|
||||
deleteContextItem(args, name) {
|
||||
try {
|
||||
this.api.deleteContextItem(args.set, args.label, name);
|
||||
} catch (ex) {
|
||||
toastr.error(ex.message);
|
||||
}
|
||||
}
|
||||
clearContextMenu(args, label) {
|
||||
try {
|
||||
this.api.clearContextMenu(args.set, args.label ?? label);
|
||||
} catch (ex) {
|
||||
toastr.error(ex.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
createSet(name, args) {
|
||||
try {
|
||||
this.api.createSet(
|
||||
args.name ?? name ?? '',
|
||||
{
|
||||
disableSend: JSON.parse(args.nosend ?? 'false') === true,
|
||||
placeBeforeInput: JSON.parse(args.before ?? 'false') === true,
|
||||
injectInput: JSON.parse(args.inject ?? 'false') === true,
|
||||
},
|
||||
);
|
||||
} catch (ex) {
|
||||
toastr.error(ex.message);
|
||||
}
|
||||
}
|
||||
updateSet(name, args) {
|
||||
try {
|
||||
this.api.updateSet(
|
||||
args.name ?? name ?? '',
|
||||
{
|
||||
disableSend: args.nosend !== undefined ? JSON.parse(args.nosend ?? 'false') === true : undefined,
|
||||
placeBeforeInput: args.before !== undefined ? JSON.parse(args.before ?? 'false') === true : undefined,
|
||||
injectInput: args.inject !== undefined ? JSON.parse(args.inject ?? 'false') === true : undefined,
|
||||
},
|
||||
);
|
||||
} catch (ex) {
|
||||
toastr.error(ex.message);
|
||||
}
|
||||
}
|
||||
deleteSet(name) {
|
||||
try {
|
||||
this.api.deleteSet(name ?? '');
|
||||
} catch (ex) {
|
||||
toastr.error(ex.message);
|
||||
}
|
||||
}
|
||||
|
||||
listSets(source) {
|
||||
try {
|
||||
switch (source) {
|
||||
case 'global':
|
||||
return this.api.listGlobalSets();
|
||||
case 'chat':
|
||||
return this.api.listChatSets();
|
||||
default:
|
||||
return this.api.listSets();
|
||||
}
|
||||
} catch (ex) {
|
||||
toastr.error(ex.message);
|
||||
}
|
||||
}
|
||||
listQuickReplies(name) {
|
||||
try {
|
||||
return this.api.listQuickReplies(name);
|
||||
} catch (ex) {
|
||||
toastr.error(ex.message);
|
||||
}
|
||||
}
|
||||
}
|
161
public/scripts/extensions/quick-reply/src/ui/ButtonUi.js
Normal file
161
public/scripts/extensions/quick-reply/src/ui/ButtonUi.js
Normal file
@ -0,0 +1,161 @@
|
||||
import { animation_duration } from '../../../../../script.js';
|
||||
import { dragElement } from '../../../../RossAscends-mods.js';
|
||||
import { loadMovingUIState } from '../../../../power-user.js';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { QuickReplySettings } from '../QuickReplySettings.js';
|
||||
|
||||
export class ButtonUi {
|
||||
/**@type {QuickReplySettings}*/ settings;
|
||||
|
||||
/**@type {HTMLElement}*/ dom;
|
||||
/**@type {HTMLElement}*/ popoutDom;
|
||||
|
||||
|
||||
|
||||
|
||||
constructor(/**@type {QuickReplySettings}*/settings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
render() {
|
||||
if (this.settings.isPopout) {
|
||||
return this.renderPopout();
|
||||
}
|
||||
return this.renderBar();
|
||||
}
|
||||
unrender() {
|
||||
this.dom?.remove();
|
||||
this.dom = null;
|
||||
this.popoutDom?.remove();
|
||||
this.popoutDom = null;
|
||||
}
|
||||
|
||||
show() {
|
||||
if (!this.settings.isEnabled) return;
|
||||
if (this.settings.isPopout) {
|
||||
document.body.append(this.render());
|
||||
loadMovingUIState();
|
||||
$(this.render()).fadeIn(animation_duration);
|
||||
dragElement($(this.render()));
|
||||
} else {
|
||||
const sendForm = document.querySelector('#send_form');
|
||||
if (sendForm.children.length > 0) {
|
||||
sendForm.children[0].insertAdjacentElement('beforebegin', this.render());
|
||||
} else {
|
||||
sendForm.append(this.render());
|
||||
}
|
||||
}
|
||||
}
|
||||
hide() {
|
||||
this.unrender();
|
||||
}
|
||||
refresh() {
|
||||
this.hide();
|
||||
this.show();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
renderBar() {
|
||||
if (!this.dom) {
|
||||
let buttonHolder;
|
||||
const root = document.createElement('div'); {
|
||||
this.dom = root;
|
||||
buttonHolder = root;
|
||||
root.id = 'qr--bar';
|
||||
root.classList.add('flex-container');
|
||||
root.classList.add('flexGap5');
|
||||
const popout = document.createElement('div'); {
|
||||
popout.id = 'qr--popoutTrigger';
|
||||
popout.classList.add('menu_button');
|
||||
popout.classList.add('fa-solid');
|
||||
popout.classList.add('fa-window-restore');
|
||||
popout.addEventListener('click', ()=>{
|
||||
this.settings.isPopout = true;
|
||||
this.refresh();
|
||||
this.settings.save();
|
||||
});
|
||||
root.append(popout);
|
||||
}
|
||||
if (this.settings.isCombined) {
|
||||
const buttons = document.createElement('div'); {
|
||||
buttonHolder = buttons;
|
||||
buttons.classList.add('qr--buttons');
|
||||
root.append(buttons);
|
||||
}
|
||||
}
|
||||
[...this.settings.config.setList, ...(this.settings.chatConfig?.setList ?? [])]
|
||||
.filter(link=>link.isVisible)
|
||||
.forEach(link=>buttonHolder.append(link.set.render()))
|
||||
;
|
||||
}
|
||||
}
|
||||
return this.dom;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
renderPopout() {
|
||||
if (!this.popoutDom) {
|
||||
let buttonHolder;
|
||||
const root = document.createElement('div'); {
|
||||
this.popoutDom = root;
|
||||
root.id = 'qr--popout';
|
||||
root.classList.add('qr--popout');
|
||||
root.classList.add('draggable');
|
||||
const head = document.createElement('div'); {
|
||||
head.classList.add('qr--header');
|
||||
root.append(head);
|
||||
const controls = document.createElement('div'); {
|
||||
controls.classList.add('qr--controls');
|
||||
controls.classList.add('panelControlBar');
|
||||
controls.classList.add('flex-container');
|
||||
const drag = document.createElement('div'); {
|
||||
drag.id = 'qr--popoutheader';
|
||||
drag.classList.add('fa-solid');
|
||||
drag.classList.add('fa-grip');
|
||||
drag.classList.add('drag-grabber');
|
||||
drag.classList.add('hoverglow');
|
||||
controls.append(drag);
|
||||
}
|
||||
const close = document.createElement('div'); {
|
||||
close.classList.add('qr--close');
|
||||
close.classList.add('fa-solid');
|
||||
close.classList.add('fa-circle-xmark');
|
||||
close.classList.add('hoverglow');
|
||||
close.addEventListener('click', ()=>{
|
||||
this.settings.isPopout = false;
|
||||
this.refresh();
|
||||
this.settings.save();
|
||||
});
|
||||
controls.append(close);
|
||||
}
|
||||
head.append(controls);
|
||||
}
|
||||
}
|
||||
const body = document.createElement('div'); {
|
||||
buttonHolder = body;
|
||||
body.classList.add('qr--body');
|
||||
if (this.settings.isCombined) {
|
||||
const buttons = document.createElement('div'); {
|
||||
buttonHolder = buttons;
|
||||
buttons.classList.add('qr--buttons');
|
||||
body.append(buttons);
|
||||
}
|
||||
}
|
||||
[...this.settings.config.setList, ...(this.settings.chatConfig?.setList ?? [])]
|
||||
.filter(link=>link.isVisible)
|
||||
.forEach(link=>buttonHolder.append(link.set.render()))
|
||||
;
|
||||
root.append(body);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.popoutDom;
|
||||
}
|
||||
}
|
366
public/scripts/extensions/quick-reply/src/ui/SettingsUi.js
Normal file
366
public/scripts/extensions/quick-reply/src/ui/SettingsUi.js
Normal file
@ -0,0 +1,366 @@
|
||||
import { callPopup } from '../../../../../script.js';
|
||||
import { getSortableDelay } from '../../../../utils.js';
|
||||
import { log, warn } from '../../index.js';
|
||||
import { QuickReply } from '../QuickReply.js';
|
||||
import { QuickReplySet } from '../QuickReplySet.js';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { QuickReplySettings } from '../QuickReplySettings.js';
|
||||
|
||||
export class SettingsUi {
|
||||
/**@type {QuickReplySettings}*/ settings;
|
||||
|
||||
/**@type {HTMLElement}*/ template;
|
||||
/**@type {HTMLElement}*/ dom;
|
||||
|
||||
/**@type {HTMLInputElement}*/ isEnabled;
|
||||
/**@type {HTMLInputElement}*/ isCombined;
|
||||
|
||||
/**@type {HTMLElement}*/ globalSetList;
|
||||
|
||||
/**@type {HTMLElement}*/ chatSetList;
|
||||
|
||||
/**@type {QuickReplySet}*/ currentQrSet;
|
||||
/**@type {HTMLInputElement}*/ disableSend;
|
||||
/**@type {HTMLInputElement}*/ placeBeforeInput;
|
||||
/**@type {HTMLInputElement}*/ injectInput;
|
||||
/**@type {HTMLSelectElement}*/ currentSet;
|
||||
|
||||
|
||||
|
||||
|
||||
constructor(/**@type {QuickReplySettings}*/settings) {
|
||||
this.settings = settings;
|
||||
settings.onRequestEditSet = (qrs) => this.selectQrSet(qrs);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
rerender() {
|
||||
if (!this.dom) return;
|
||||
const content = this.dom.querySelector('.inline-drawer-content');
|
||||
content.innerHTML = '';
|
||||
// @ts-ignore
|
||||
Array.from(this.template.querySelector('.inline-drawer-content').cloneNode(true).children).forEach(el=>{
|
||||
content.append(el);
|
||||
});
|
||||
this.prepareDom();
|
||||
}
|
||||
unrender() {
|
||||
this.dom?.remove();
|
||||
this.dom = null;
|
||||
}
|
||||
async render() {
|
||||
if (!this.dom) {
|
||||
const response = await fetch('/scripts/extensions/quick-reply/html/settings.html', { cache: 'no-store' });
|
||||
if (response.ok) {
|
||||
this.template = document.createRange().createContextualFragment(await response.text()).querySelector('#qr--settings');
|
||||
// @ts-ignore
|
||||
this.dom = this.template.cloneNode(true);
|
||||
this.prepareDom();
|
||||
} else {
|
||||
warn('failed to fetch settings template');
|
||||
}
|
||||
}
|
||||
return this.dom;
|
||||
}
|
||||
|
||||
|
||||
prepareGeneralSettings() {
|
||||
// general settings
|
||||
this.isEnabled = this.dom.querySelector('#qr--isEnabled');
|
||||
this.isEnabled.checked = this.settings.isEnabled;
|
||||
this.isEnabled.addEventListener('click', ()=>this.onIsEnabled());
|
||||
|
||||
this.isCombined = this.dom.querySelector('#qr--isCombined');
|
||||
this.isCombined.checked = this.settings.isCombined;
|
||||
this.isCombined.addEventListener('click', ()=>this.onIsCombined());
|
||||
}
|
||||
|
||||
prepareGlobalSetList() {
|
||||
const dom = this.template.querySelector('#qr--global');
|
||||
const clone = dom.cloneNode(true);
|
||||
// @ts-ignore
|
||||
this.settings.config.renderSettingsInto(clone);
|
||||
this.dom.querySelector('#qr--global').replaceWith(clone);
|
||||
}
|
||||
prepareChatSetList() {
|
||||
const dom = this.template.querySelector('#qr--chat');
|
||||
const clone = dom.cloneNode(true);
|
||||
if (this.settings.chatConfig) {
|
||||
// @ts-ignore
|
||||
this.settings.chatConfig.renderSettingsInto(clone);
|
||||
} else {
|
||||
const info = document.createElement('div'); {
|
||||
info.textContent = 'No active chat.';
|
||||
// @ts-ignore
|
||||
clone.append(info);
|
||||
}
|
||||
}
|
||||
this.dom.querySelector('#qr--chat').replaceWith(clone);
|
||||
}
|
||||
|
||||
prepareQrEditor() {
|
||||
// qr editor
|
||||
this.dom.querySelector('#qr--set-new').addEventListener('click', async()=>this.addQrSet());
|
||||
/**@type {HTMLInputElement}*/
|
||||
const importFile = this.dom.querySelector('#qr--set-importFile');
|
||||
importFile.addEventListener('change', async()=>{
|
||||
await this.importQrSet(importFile.files);
|
||||
importFile.value = null;
|
||||
});
|
||||
this.dom.querySelector('#qr--set-import').addEventListener('click', ()=>importFile.click());
|
||||
this.dom.querySelector('#qr--set-export').addEventListener('click', async()=>this.exportQrSet());
|
||||
this.dom.querySelector('#qr--set-delete').addEventListener('click', async()=>this.deleteQrSet());
|
||||
this.dom.querySelector('#qr--set-add').addEventListener('click', async()=>{
|
||||
this.currentQrSet.addQuickReply();
|
||||
});
|
||||
this.qrList = this.dom.querySelector('#qr--set-qrList');
|
||||
this.currentSet = this.dom.querySelector('#qr--set');
|
||||
this.currentSet.addEventListener('change', ()=>this.onQrSetChange());
|
||||
QuickReplySet.list.forEach(qrs=>{
|
||||
const opt = document.createElement('option'); {
|
||||
opt.value = qrs.name;
|
||||
opt.textContent = qrs.name;
|
||||
this.currentSet.append(opt);
|
||||
}
|
||||
});
|
||||
this.disableSend = this.dom.querySelector('#qr--disableSend');
|
||||
this.disableSend.addEventListener('click', ()=>{
|
||||
const qrs = this.currentQrSet;
|
||||
qrs.disableSend = this.disableSend.checked;
|
||||
qrs.save();
|
||||
});
|
||||
this.placeBeforeInput = this.dom.querySelector('#qr--placeBeforeInput');
|
||||
this.placeBeforeInput.addEventListener('click', ()=>{
|
||||
const qrs = this.currentQrSet;
|
||||
qrs.placeBeforeInput = this.placeBeforeInput.checked;
|
||||
qrs.save();
|
||||
});
|
||||
this.injectInput = this.dom.querySelector('#qr--injectInput');
|
||||
this.injectInput.addEventListener('click', ()=>{
|
||||
const qrs = this.currentQrSet;
|
||||
qrs.injectInput = this.injectInput.checked;
|
||||
qrs.save();
|
||||
});
|
||||
this.onQrSetChange();
|
||||
}
|
||||
onQrSetChange() {
|
||||
this.currentQrSet = QuickReplySet.get(this.currentSet.value);
|
||||
this.disableSend.checked = this.currentQrSet.disableSend;
|
||||
this.placeBeforeInput.checked = this.currentQrSet.placeBeforeInput;
|
||||
this.injectInput.checked = this.currentQrSet.injectInput;
|
||||
this.qrList.innerHTML = '';
|
||||
const qrsDom = this.currentQrSet.renderSettings();
|
||||
this.qrList.append(qrsDom);
|
||||
// @ts-ignore
|
||||
$(qrsDom).sortable({
|
||||
delay: getSortableDelay(),
|
||||
handle: '.drag-handle',
|
||||
stop: ()=>this.onQrListSort(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
prepareDom() {
|
||||
this.prepareGeneralSettings();
|
||||
this.prepareGlobalSetList();
|
||||
this.prepareChatSetList();
|
||||
this.prepareQrEditor();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
async onIsEnabled() {
|
||||
this.settings.isEnabled = this.isEnabled.checked;
|
||||
this.settings.save();
|
||||
}
|
||||
|
||||
async onIsCombined() {
|
||||
this.settings.isCombined = this.isCombined.checked;
|
||||
this.settings.save();
|
||||
}
|
||||
|
||||
async onGlobalSetListSort() {
|
||||
this.settings.config.setList = Array.from(this.globalSetList.children).map((it,idx)=>{
|
||||
const set = this.settings.config.setList[Number(it.getAttribute('data-order'))];
|
||||
it.setAttribute('data-order', String(idx));
|
||||
return set;
|
||||
});
|
||||
this.settings.save();
|
||||
}
|
||||
|
||||
async onChatSetListSort() {
|
||||
this.settings.chatConfig.setList = Array.from(this.chatSetList.children).map((it,idx)=>{
|
||||
const set = this.settings.chatConfig.setList[Number(it.getAttribute('data-order'))];
|
||||
it.setAttribute('data-order', String(idx));
|
||||
return set;
|
||||
});
|
||||
this.settings.save();
|
||||
}
|
||||
|
||||
updateOrder(list) {
|
||||
Array.from(list.children).forEach((it,idx)=>{
|
||||
it.setAttribute('data-order', idx);
|
||||
});
|
||||
}
|
||||
|
||||
async onQrListSort() {
|
||||
this.currentQrSet.qrList = Array.from(this.qrList.querySelectorAll('.qr--set-item')).map((it,idx)=>{
|
||||
const qr = this.currentQrSet.qrList.find(qr=>qr.id == Number(it.getAttribute('data-id')));
|
||||
it.setAttribute('data-order', String(idx));
|
||||
return qr;
|
||||
});
|
||||
this.currentQrSet.save();
|
||||
}
|
||||
|
||||
async deleteQrSet() {
|
||||
const confirmed = await callPopup(`Are you sure you want to delete the Quick Reply Set "${this.currentQrSet.name}"?<br>This cannot be undone.`, 'confirm');
|
||||
if (confirmed) {
|
||||
await this.doDeleteQrSet(this.currentQrSet);
|
||||
this.rerender();
|
||||
}
|
||||
}
|
||||
async doDeleteQrSet(qrs) {
|
||||
await qrs.delete();
|
||||
//TODO (HACK) should just bubble up from QuickReplySet.delete() but that would require proper or at least more comples onDelete listeners
|
||||
for (let i = this.settings.config.setList.length - 1; i >= 0; i--) {
|
||||
if (this.settings.config.setList[i].set == qrs) {
|
||||
this.settings.config.setList.splice(i, 1);
|
||||
}
|
||||
}
|
||||
if (this.settings.chatConfig) {
|
||||
for (let i = this.settings.chatConfig.setList.length - 1; i >= 0; i--) {
|
||||
if (this.settings.chatConfig.setList[i].set == qrs) {
|
||||
this.settings.chatConfig.setList.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.settings.save();
|
||||
}
|
||||
|
||||
async addQrSet() {
|
||||
const name = await callPopup('Quick Reply Set Name:', 'input');
|
||||
if (name && name.length > 0) {
|
||||
const oldQrs = QuickReplySet.get(name);
|
||||
if (oldQrs) {
|
||||
const replace = await callPopup(`A Quick Reply Set named "${name}" already exists.<br>Do you want to overwrite the existing Quick Reply Set?<br>The existing set will be deleted. This cannot be undone.`, 'confirm');
|
||||
if (replace) {
|
||||
const idx = QuickReplySet.list.indexOf(oldQrs);
|
||||
await this.doDeleteQrSet(oldQrs);
|
||||
const qrs = new QuickReplySet();
|
||||
qrs.name = name;
|
||||
qrs.addQuickReply();
|
||||
QuickReplySet.list.splice(idx, 0, qrs);
|
||||
this.rerender();
|
||||
this.currentSet.value = name;
|
||||
this.onQrSetChange();
|
||||
this.prepareGlobalSetList();
|
||||
this.prepareChatSetList();
|
||||
}
|
||||
} else {
|
||||
const qrs = new QuickReplySet();
|
||||
qrs.name = name;
|
||||
qrs.addQuickReply();
|
||||
const idx = QuickReplySet.list.findIndex(it=>it.name.localeCompare(name) == 1);
|
||||
if (idx > -1) {
|
||||
QuickReplySet.list.splice(idx, 0, qrs);
|
||||
} else {
|
||||
QuickReplySet.list.push(qrs);
|
||||
}
|
||||
const opt = document.createElement('option'); {
|
||||
opt.value = qrs.name;
|
||||
opt.textContent = qrs.name;
|
||||
if (idx > -1) {
|
||||
this.currentSet.children[idx].insertAdjacentElement('beforebegin', opt);
|
||||
} else {
|
||||
this.currentSet.append(opt);
|
||||
}
|
||||
}
|
||||
this.currentSet.value = name;
|
||||
this.onQrSetChange();
|
||||
this.prepareGlobalSetList();
|
||||
this.prepareChatSetList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async importQrSet(/**@type {FileList}*/files) {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
await this.importSingleQrSet(files.item(i));
|
||||
}
|
||||
}
|
||||
async importSingleQrSet(/**@type {File}*/file) {
|
||||
log('FILE', file);
|
||||
try {
|
||||
const text = await file.text();
|
||||
const props = JSON.parse(text);
|
||||
if (!Number.isInteger(props.version) || typeof props.name != 'string') {
|
||||
toastr.error(`The file "${file.name}" does not appear to be a valid quick reply set.`);
|
||||
warn(`The file "${file.name}" does not appear to be a valid quick reply set.`);
|
||||
} else {
|
||||
/**@type {QuickReplySet}*/
|
||||
const qrs = QuickReplySet.from(JSON.parse(JSON.stringify(props)));
|
||||
qrs.qrList = props.qrList.map(it=>QuickReply.from(it));
|
||||
qrs.init();
|
||||
const oldQrs = QuickReplySet.get(props.name);
|
||||
if (oldQrs) {
|
||||
const replace = await callPopup(`A Quick Reply Set named "${qrs.name}" already exists.<br>Do you want to overwrite the existing Quick Reply Set?<br>The existing set will be deleted. This cannot be undone.`, 'confirm');
|
||||
if (replace) {
|
||||
const idx = QuickReplySet.list.indexOf(oldQrs);
|
||||
await this.doDeleteQrSet(oldQrs);
|
||||
QuickReplySet.list.splice(idx, 0, qrs);
|
||||
await qrs.save();
|
||||
this.rerender();
|
||||
this.currentSet.value = qrs.name;
|
||||
this.onQrSetChange();
|
||||
this.prepareGlobalSetList();
|
||||
this.prepareChatSetList();
|
||||
}
|
||||
} else {
|
||||
const idx = QuickReplySet.list.findIndex(it=>it.name.localeCompare(qrs.name) == 1);
|
||||
if (idx > -1) {
|
||||
QuickReplySet.list.splice(idx, 0, qrs);
|
||||
} else {
|
||||
QuickReplySet.list.push(qrs);
|
||||
}
|
||||
await qrs.save();
|
||||
const opt = document.createElement('option'); {
|
||||
opt.value = qrs.name;
|
||||
opt.textContent = qrs.name;
|
||||
if (idx > -1) {
|
||||
this.currentSet.children[idx].insertAdjacentElement('beforebegin', opt);
|
||||
} else {
|
||||
this.currentSet.append(opt);
|
||||
}
|
||||
}
|
||||
this.currentSet.value = qrs.name;
|
||||
this.onQrSetChange();
|
||||
this.prepareGlobalSetList();
|
||||
this.prepareChatSetList();
|
||||
}
|
||||
}
|
||||
} catch (ex) {
|
||||
warn(ex);
|
||||
toastr.error(`Failed to import "${file.name}":\n\n${ex.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
exportQrSet() {
|
||||
const blob = new Blob([JSON.stringify(this.currentQrSet)], { type:'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a'); {
|
||||
a.href = url;
|
||||
a.download = `${this.currentQrSet.name}.json`;
|
||||
a.click();
|
||||
}
|
||||
}
|
||||
|
||||
selectQrSet(qrs) {
|
||||
this.currentSet.value = qrs.name;
|
||||
this.onQrSetChange();
|
||||
}
|
||||
}
|
108
public/scripts/extensions/quick-reply/src/ui/ctx/ContextMenu.js
Normal file
108
public/scripts/extensions/quick-reply/src/ui/ctx/ContextMenu.js
Normal file
@ -0,0 +1,108 @@
|
||||
import { QuickReply } from '../../QuickReply.js';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { QuickReplySet } from '../../QuickReplySet.js';
|
||||
import { MenuHeader } from './MenuHeader.js';
|
||||
import { MenuItem } from './MenuItem.js';
|
||||
|
||||
export class ContextMenu {
|
||||
/**@type {MenuItem[]}*/ itemList = [];
|
||||
/**@type {Boolean}*/ isActive = false;
|
||||
|
||||
/**@type {HTMLElement}*/ root;
|
||||
/**@type {HTMLElement}*/ menu;
|
||||
|
||||
|
||||
|
||||
|
||||
constructor(/**@type {QuickReply}*/qr) {
|
||||
// this.itemList = items;
|
||||
this.itemList = this.build(qr).children;
|
||||
this.itemList.forEach(item => {
|
||||
item.onExpand = () => {
|
||||
this.itemList.filter(it => it != item)
|
||||
.forEach(it => it.collapse());
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {QuickReply} qr
|
||||
* @param {String} chainedMessage
|
||||
* @param {QuickReplySet[]} hierarchy
|
||||
* @param {String[]} labelHierarchy
|
||||
*/
|
||||
build(qr, chainedMessage = null, hierarchy = [], labelHierarchy = []) {
|
||||
const tree = {
|
||||
label: qr.label,
|
||||
message: (chainedMessage && qr.message ? `${chainedMessage} | ` : '') + qr.message,
|
||||
children: [],
|
||||
};
|
||||
qr.contextList.forEach((cl) => {
|
||||
if (!hierarchy.includes(cl.set)) {
|
||||
const nextHierarchy = [...hierarchy, cl.set];
|
||||
const nextLabelHierarchy = [...labelHierarchy, tree.label];
|
||||
tree.children.push(new MenuHeader(cl.set.name));
|
||||
cl.set.qrList.forEach(subQr => {
|
||||
const subTree = this.build(subQr, cl.isChained ? tree.message : null, nextHierarchy, nextLabelHierarchy);
|
||||
tree.children.push(new MenuItem(
|
||||
subTree.label,
|
||||
subTree.message,
|
||||
(evt) => {
|
||||
evt.stopPropagation();
|
||||
const finalQr = Object.assign(new QuickReply(), subQr);
|
||||
finalQr.message = subTree.message.replace(/%%parent(-\d+)?%%/g, (_, index) => {
|
||||
return nextLabelHierarchy.slice(parseInt(index ?? '-1'))[0];
|
||||
});
|
||||
cl.set.execute(finalQr);
|
||||
},
|
||||
subTree.children,
|
||||
));
|
||||
});
|
||||
}
|
||||
});
|
||||
return tree;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.root) {
|
||||
const blocker = document.createElement('div'); {
|
||||
this.root = blocker;
|
||||
blocker.classList.add('ctx-blocker');
|
||||
blocker.addEventListener('click', () => this.hide());
|
||||
const menu = document.createElement('ul'); {
|
||||
this.menu = menu;
|
||||
menu.classList.add('list-group');
|
||||
menu.classList.add('ctx-menu');
|
||||
this.itemList.forEach(it => menu.append(it.render()));
|
||||
blocker.append(menu);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.root;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
show({ clientX, clientY }) {
|
||||
if (this.isActive) return;
|
||||
this.isActive = true;
|
||||
this.render();
|
||||
this.menu.style.bottom = `${window.innerHeight - clientY}px`;
|
||||
this.menu.style.left = `${clientX}px`;
|
||||
document.body.append(this.root);
|
||||
}
|
||||
hide() {
|
||||
if (this.root) {
|
||||
this.root.remove();
|
||||
}
|
||||
this.isActive = false;
|
||||
}
|
||||
toggle(/**@type {PointerEvent}*/evt) {
|
||||
if (this.isActive) {
|
||||
this.hide();
|
||||
} else {
|
||||
this.show(evt);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,114 +1,286 @@
|
||||
#quickReplyBar {
|
||||
outline: none;
|
||||
/*
|
||||
padding: 5px 0;
|
||||
border-bottom: 1px solid var(--SmartThemeBorderColor);
|
||||
*/
|
||||
margin: 0;
|
||||
transition: 0.3s;
|
||||
opacity: 0.7;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
display: none;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
order: 1;
|
||||
position: relative;
|
||||
#qr--bar {
|
||||
outline: none;
|
||||
margin: 0;
|
||||
transition: 0.3s;
|
||||
opacity: 0.7;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
order: 1;
|
||||
padding-right: 2.5em;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#quickReplies {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
width: 100%;
|
||||
#qr--bar > #qr--popoutTrigger {
|
||||
position: absolute;
|
||||
right: 0.25em;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
#quickReplyPopoutButton {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 0px;
|
||||
#qr--popout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
z-index: 31;
|
||||
}
|
||||
|
||||
#quickReplies div {
|
||||
color: var(--SmartThemeBodyColor);
|
||||
background-color: var(--black50a);
|
||||
border: 1px solid var(--SmartThemeBorderColor);
|
||||
border-radius: 10px;
|
||||
padding: 3px 5px;
|
||||
margin: 3px 0;
|
||||
/* width: min-content; */
|
||||
cursor: pointer;
|
||||
transition: 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
#qr--popout > .qr--header {
|
||||
flex: 0 0 auto;
|
||||
height: 2em;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#quickReplies div:hover {
|
||||
opacity: 1;
|
||||
filter: brightness(1.2);
|
||||
cursor: pointer;
|
||||
#qr--popout > .qr--header > .qr--controls > .qr--close {
|
||||
height: 15px;
|
||||
aspect-ratio: 1 / 1;
|
||||
font-size: 20px;
|
||||
opacity: 0.5;
|
||||
transition: all 250ms;
|
||||
}
|
||||
#qr--popout > .qr--body {
|
||||
overflow-y: auto;
|
||||
}
|
||||
#qr--bar > .qr--buttons,
|
||||
#qr--popout > .qr--body > .qr--buttons {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
width: 100%;
|
||||
}
|
||||
#qr--bar > .qr--buttons > .qr--buttons,
|
||||
#qr--popout > .qr--body > .qr--buttons > .qr--buttons {
|
||||
display: contents;
|
||||
}
|
||||
#qr--bar > .qr--buttons .qr--button,
|
||||
#qr--popout > .qr--body > .qr--buttons .qr--button {
|
||||
color: var(--SmartThemeBodyColor);
|
||||
border: 1px solid var(--SmartThemeBorderColor);
|
||||
border-radius: 10px;
|
||||
padding: 3px 5px;
|
||||
margin: 3px 0;
|
||||
cursor: pointer;
|
||||
transition: 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
#qr--bar > .qr--buttons .qr--button:hover,
|
||||
#qr--popout > .qr--body > .qr--buttons .qr--button:hover {
|
||||
opacity: 1;
|
||||
filter: brightness(1.2);
|
||||
}
|
||||
#qr--bar > .qr--buttons .qr--button > .qr--button-expander,
|
||||
#qr--popout > .qr--body > .qr--buttons .qr--button > .qr--button-expander {
|
||||
display: none;
|
||||
}
|
||||
#qr--bar > .qr--buttons .qr--button.qr--hasCtx > .qr--button-expander,
|
||||
#qr--popout > .qr--body > .qr--buttons .qr--button.qr--hasCtx > .qr--button-expander {
|
||||
display: block;
|
||||
}
|
||||
.qr--button-expander {
|
||||
border-left: 1px solid;
|
||||
margin-left: 1em;
|
||||
text-align: center;
|
||||
width: 2em;
|
||||
}
|
||||
.qr--button-expander:hover {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
.ctx-blocker {
|
||||
/* backdrop-filter: blur(1px); */
|
||||
/* background-color: rgba(0 0 0 / 10%); */
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 999;
|
||||
/* backdrop-filter: blur(1px); */
|
||||
/* background-color: rgba(0 0 0 / 10%); */
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.ctx-menu {
|
||||
position: absolute;
|
||||
overflow: visible;
|
||||
position: absolute;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.list-group .list-group-item.ctx-header {
|
||||
font-weight: bold;
|
||||
cursor: default;
|
||||
font-weight: bold;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.ctx-item+.ctx-header {
|
||||
border-top: 1px solid;
|
||||
.ctx-item + .ctx-header {
|
||||
border-top: 1px solid;
|
||||
}
|
||||
|
||||
.ctx-item {
|
||||
position: relative;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ctx-expander {
|
||||
border-left: 1px solid;
|
||||
margin-left: 1em;
|
||||
text-align: center;
|
||||
width: 2em;
|
||||
border-left: 1px solid;
|
||||
margin-left: 1em;
|
||||
text-align: center;
|
||||
width: 2em;
|
||||
}
|
||||
|
||||
.ctx-expander:hover {
|
||||
font-weight: bold;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ctx-sub-menu {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1000px) {
|
||||
.ctx-blocker {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.list-group .list-group-item.ctx-item {
|
||||
padding: 1em;
|
||||
}
|
||||
.ctx-blocker {
|
||||
position: absolute;
|
||||
}
|
||||
.list-group .list-group-item.ctx-item {
|
||||
padding: 1em;
|
||||
}
|
||||
}
|
||||
#qr--settings .qr--head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 1em;
|
||||
}
|
||||
#qr--settings .qr--head > .qr--title {
|
||||
font-weight: bold;
|
||||
}
|
||||
#qr--settings .qr--head > .qr--actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
gap: 0.5em;
|
||||
}
|
||||
#qr--settings .qr--setList > .qr--item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5em;
|
||||
align-items: baseline;
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
#qr--settings .qr--setList > .qr--item > .drag-handle {
|
||||
padding: 0.75em;
|
||||
}
|
||||
#qr--settings .qr--setList > .qr--item > .qr--visible {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
#qr--settings #qr--set-settings #qr--injectInputContainer {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
#qr--settings #qr--set-qrList .qr--set-qrListContents {
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5em;
|
||||
align-items: baseline;
|
||||
padding: 0.25em 0;
|
||||
}
|
||||
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(1) {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(2) {
|
||||
flex: 1 1 25%;
|
||||
}
|
||||
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(3) {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(4) {
|
||||
flex: 1 1 75%;
|
||||
}
|
||||
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(5) {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > .drag-handle {
|
||||
padding: 0.75em;
|
||||
}
|
||||
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemLabel,
|
||||
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--action {
|
||||
margin: 0;
|
||||
}
|
||||
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemMessage {
|
||||
font-size: smaller;
|
||||
}
|
||||
#qr--settings .qr--set-qrListActions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5em;
|
||||
justify-content: center;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
#qr--qrOptions > #qr--ctxEditor .qr--ctxItem {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5em;
|
||||
align-items: baseline;
|
||||
}
|
||||
@media screen and (max-width: 750px) {
|
||||
body #dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor {
|
||||
flex-direction: column;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
#dialogue_popup:has(#qr--modalEditor) {
|
||||
aspect-ratio: unset;
|
||||
}
|
||||
#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1em;
|
||||
}
|
||||
#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5em;
|
||||
}
|
||||
#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label {
|
||||
flex: 1 1 1px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelText {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelHint {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label > input {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-message {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor #qr--modal-execute {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5em;
|
||||
}
|
||||
#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor #qr--modal-execute.qr--busy {
|
||||
opacity: 0.5;
|
||||
cursor: wait;
|
||||
}
|
||||
#shadow_popup.qr--hide {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
317
public/scripts/extensions/quick-reply/style.less
Normal file
317
public/scripts/extensions/quick-reply/style.less
Normal file
@ -0,0 +1,317 @@
|
||||
#qr--bar {
|
||||
outline: none;
|
||||
margin: 0;
|
||||
transition: 0.3s;
|
||||
opacity: 0.7;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
order: 1;
|
||||
padding-right: 2.5em;
|
||||
position: relative;
|
||||
> #qr--popoutTrigger {
|
||||
position: absolute;
|
||||
right: 0.25em;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
#qr--popout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
z-index: 31;
|
||||
> .qr--header {
|
||||
flex: 0 0 auto;
|
||||
height: 2em;
|
||||
position: relative;
|
||||
> .qr--controls {
|
||||
> .qr--close {
|
||||
height: 15px;
|
||||
aspect-ratio: 1 / 1;
|
||||
font-size: 20px;
|
||||
opacity: 0.5;
|
||||
transition: all 250ms;
|
||||
}
|
||||
}
|
||||
}
|
||||
> .qr--body {
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
#qr--bar, #qr--popout > .qr--body {
|
||||
> .qr--buttons {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
width: 100%;
|
||||
|
||||
> .qr--buttons {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.qr--button {
|
||||
color: var(--SmartThemeBodyColor);
|
||||
// background-color: var(--black50a);
|
||||
border: 1px solid var(--SmartThemeBorderColor);
|
||||
border-radius: 10px;
|
||||
padding: 3px 5px;
|
||||
margin: 3px 0;
|
||||
cursor: pointer;
|
||||
transition: 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
filter: brightness(1.2);
|
||||
}
|
||||
> .qr--button-expander {
|
||||
display: none;
|
||||
}
|
||||
&.qr--hasCtx {
|
||||
> .qr--button-expander {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.qr--button-expander {
|
||||
border-left: 1px solid;
|
||||
margin-left: 1em;
|
||||
text-align: center;
|
||||
width: 2em;
|
||||
&:hover {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.ctx-blocker {
|
||||
/* backdrop-filter: blur(1px); */
|
||||
/* background-color: rgba(0 0 0 / 10%); */
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.ctx-menu {
|
||||
position: absolute;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.list-group .list-group-item.ctx-header {
|
||||
font-weight: bold;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.ctx-item+.ctx-header {
|
||||
border-top: 1px solid;
|
||||
}
|
||||
|
||||
.ctx-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ctx-expander {
|
||||
border-left: 1px solid;
|
||||
margin-left: 1em;
|
||||
text-align: center;
|
||||
width: 2em;
|
||||
}
|
||||
|
||||
.ctx-expander:hover {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ctx-sub-menu {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1000px) {
|
||||
.ctx-blocker {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.list-group .list-group-item.ctx-item {
|
||||
padding: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#qr--settings {
|
||||
.qr--head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 1em;
|
||||
> .qr--title {
|
||||
font-weight: bold;
|
||||
}
|
||||
> .qr--actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
gap: 0.5em;
|
||||
}
|
||||
}
|
||||
.qr--setList {
|
||||
> .qr--item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5em;
|
||||
align-items: baseline;
|
||||
padding: 0 0.5em;
|
||||
> .drag-handle {
|
||||
padding: 0.75em;
|
||||
}
|
||||
> .qr--visible {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
}
|
||||
#qr--set-settings {
|
||||
#qr--injectInputContainer {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
#qr--set-qrList {
|
||||
.qr--set-qrListContents > {
|
||||
padding: 0 0.5em;
|
||||
> .qr--set-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5em;
|
||||
align-items: baseline;
|
||||
padding: 0.25em 0;
|
||||
> :nth-child(1) { flex: 0 0 auto; }
|
||||
> :nth-child(2) { flex: 1 1 25%; }
|
||||
> :nth-child(3) { flex: 0 0 auto; }
|
||||
> :nth-child(4) { flex: 1 1 75%; }
|
||||
> :nth-child(5) { flex: 0 0 auto; }
|
||||
> .drag-handle {
|
||||
padding: 0.75em;
|
||||
}
|
||||
.qr--set-itemLabel, .qr--action {
|
||||
margin: 0;
|
||||
}
|
||||
.qr--set-itemMessage {
|
||||
font-size: smaller;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.qr--set-qrListActions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5em;
|
||||
justify-content: center;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#qr--qrOptions {
|
||||
> #qr--ctxEditor {
|
||||
.qr--ctxItem {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5em;
|
||||
align-items: baseline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@media screen and (max-width: 750px) {
|
||||
body #dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor {
|
||||
flex-direction: column;
|
||||
> #qr--main > .qr--labels {
|
||||
flex-direction: column;
|
||||
}
|
||||
> #qr--main > .qr--modal-messageContainer > #qr--modal-message {
|
||||
min-height: 90svh;
|
||||
}
|
||||
}
|
||||
}
|
||||
#dialogue_popup:has(#qr--modalEditor) {
|
||||
aspect-ratio: unset;
|
||||
|
||||
#dialogue_popup_text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> #qr--modalEditor {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1em;
|
||||
|
||||
> #qr--main {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
> .qr--labels {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5em;
|
||||
> label {
|
||||
flex: 1 1 1px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
> .qr--labelText {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
> .qr--labelHint {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
> input {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
> .qr--modal-messageContainer {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
> #qr--modal-message {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#qr--modal-execute {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5em;
|
||||
&.qr--busy {
|
||||
opacity: 0.5;
|
||||
cursor: wait;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#shadow_popup.qr--hide {
|
||||
opacity: 0 !important;
|
||||
}
|
@ -56,7 +56,7 @@
|
||||
<div>
|
||||
<textarea
|
||||
class="regex_replace_string text_pole wide100p textarea_compact"
|
||||
placeholder="Use {{match}} to include the matched text from the Find Regex"
|
||||
placeholder="Use {{match}} to include the matched text from the Find Regex or $1, $2, etc. for capture groups."
|
||||
rows="2"
|
||||
></textarea>
|
||||
</div>
|
||||
@ -115,16 +115,16 @@
|
||||
<input type="checkbox" name="run_on_edit" />
|
||||
<span data-i18n="Run On Edit">Run On Edit</span>
|
||||
</label>
|
||||
<label class="checkbox flex-container">
|
||||
<label class="checkbox flex-container" title="Substitute {{macros}} in Find Regex before running it">
|
||||
<input type="checkbox" name="substitute_regex" />
|
||||
<span data-i18n="Substitute Regex">Substitute Regex</span>
|
||||
<span data-i18n="Substitute Regex">Substitute Regex (?)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex-container flexFlowColumn alignitemsstart">
|
||||
<small>Replacement Strategy</small>
|
||||
<select name="replace_strategy_select" class="margin0">
|
||||
<option value="0">Replace</option>
|
||||
<option value="1">Overlay</option>
|
||||
<option value="1">Overlay (currently broken)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -109,19 +109,29 @@ function runRegexScript(regexScript, rawString, { characterOverride } = {}) {
|
||||
return newString;
|
||||
}
|
||||
|
||||
newString = rawString.replace(findRegex, (fencedMatch) => {
|
||||
let trimFencedMatch = filterString(fencedMatch, regexScript.trimStrings, { characterOverride });
|
||||
// Run replacement. Currently does not support the Overlay strategy
|
||||
newString = rawString.replace(findRegex, function(match) {
|
||||
const args = [...arguments];
|
||||
const replaceString = regexScript.replaceString.replace(/{{match}}/gi, '$0');
|
||||
const replaceWithGroups = replaceString.replaceAll(/\$(\d)+/g, (_, num) => {
|
||||
// Get a full match or a capture group
|
||||
const match = args[Number(num)];
|
||||
|
||||
const subReplaceString = substituteRegexParams(
|
||||
regexScript.replaceString,
|
||||
trimFencedMatch,
|
||||
{
|
||||
characterOverride,
|
||||
replaceStrategy: regexScript.replaceStrategy ?? regex_replace_strategy.REPLACE,
|
||||
},
|
||||
);
|
||||
// No match found - return the empty string
|
||||
if (!match) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return subReplaceString;
|
||||
// Remove trim strings from the match
|
||||
const filteredMatch = filterString(match, regexScript.trimStrings, { characterOverride });
|
||||
|
||||
// TODO: Handle overlay here
|
||||
|
||||
return filteredMatch;
|
||||
});
|
||||
|
||||
// Substitute at the end
|
||||
return substituteParams(replaceWithGroups);
|
||||
});
|
||||
|
||||
return newString;
|
||||
|
@ -331,7 +331,7 @@ export function formatInstructModeExamples(mesExamples, name1, name2) {
|
||||
* @returns {string} Formatted instruct mode last prompt line.
|
||||
*/
|
||||
export function formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2) {
|
||||
const includeNames = power_user.instruct.names || (!!selected_group && power_user.instruct.names_force_groups);
|
||||
const includeNames = name && (power_user.instruct.names || (!!selected_group && power_user.instruct.names_force_groups));
|
||||
const getOutputSequence = () => power_user.instruct.last_output_sequence || power_user.instruct.output_sequence;
|
||||
let sequence = isImpersonate ? power_user.instruct.input_sequence : getOutputSequence();
|
||||
|
||||
|
@ -308,6 +308,11 @@ const sliders = [
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Sets the supported feature flags for the KoboldAI backend.
|
||||
* @param {string} koboldUnitedVersion Kobold United version
|
||||
* @param {string} koboldCppVersion KoboldCPP version
|
||||
*/
|
||||
export function setKoboldFlags(koboldUnitedVersion, koboldCppVersion) {
|
||||
kai_flags.can_use_stop_sequence = versionCompare(koboldUnitedVersion, MIN_STOP_SEQUENCE_VERSION);
|
||||
kai_flags.can_use_streaming = versionCompare(koboldCppVersion, MIN_STREAMING_KCPPVERSION);
|
||||
@ -316,6 +321,8 @@ export function setKoboldFlags(koboldUnitedVersion, koboldCppVersion) {
|
||||
kai_flags.can_use_mirostat = versionCompare(koboldCppVersion, MIN_MIROSTAT_KCPPVERSION);
|
||||
kai_flags.can_use_grammar = versionCompare(koboldCppVersion, MIN_GRAMMAR_KCPPVERSION);
|
||||
kai_flags.can_use_min_p = versionCompare(koboldCppVersion, MIN_MIN_P_KCPPVERSION);
|
||||
const isKoboldCpp = versionCompare(koboldCppVersion, '1.0.0');
|
||||
$('#koboldcpp_hint').toggleClass('displayNone', !isKoboldCpp);
|
||||
}
|
||||
|
||||
/**
|
||||
|
276
public/scripts/macros.js
Normal file
276
public/scripts/macros.js
Normal file
@ -0,0 +1,276 @@
|
||||
import { chat, main_api, getMaxContextSize, getCharacterCardFields } from '../script.js';
|
||||
import { timestampToMoment, isDigitsOnly } from './utils.js';
|
||||
import { textgenerationwebui_banned_in_macros } from './textgen-settings.js';
|
||||
import { replaceInstructMacros } from './instruct-mode.js';
|
||||
import { replaceVariableMacros } from './variables.js';
|
||||
|
||||
/**
|
||||
* Returns the ID of the last message in the chat.
|
||||
* @returns {string} The ID of the last message in the chat.
|
||||
*/
|
||||
function getLastMessageId() {
|
||||
const index = chat?.length - 1;
|
||||
|
||||
if (!isNaN(index) && index >= 0) {
|
||||
return String(index);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID of the first message included in the context.
|
||||
* @returns {string} The ID of the first message in the context.
|
||||
*/
|
||||
function getFirstIncludedMessageId() {
|
||||
const index = document.querySelector('.lastInContext')?.getAttribute('mesid');
|
||||
|
||||
if (!isNaN(index) && index >= 0) {
|
||||
return String(index);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last message in the chat.
|
||||
* @returns {string} The last message in the chat.
|
||||
*/
|
||||
function getLastMessage() {
|
||||
const index = chat?.length - 1;
|
||||
|
||||
if (!isNaN(index) && index >= 0) {
|
||||
return chat[index].mes;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID of the last swipe.
|
||||
* @returns {string} The 1-based ID of the last swipe
|
||||
*/
|
||||
function getLastSwipeId() {
|
||||
const index = chat?.length - 1;
|
||||
|
||||
if (!isNaN(index) && index >= 0) {
|
||||
const swipes = chat[index].swipes;
|
||||
|
||||
if (!Array.isArray(swipes) || swipes.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return String(swipes.length);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID of the current swipe.
|
||||
* @returns {string} The 1-based ID of the current swipe.
|
||||
*/
|
||||
function getCurrentSwipeId() {
|
||||
const index = chat?.length - 1;
|
||||
|
||||
if (!isNaN(index) && index >= 0) {
|
||||
const swipeId = chat[index].swipe_id;
|
||||
|
||||
if (swipeId === undefined || isNaN(swipeId)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return String(swipeId + 1);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces banned words in macros with an empty string.
|
||||
* Adds them to textgenerationwebui ban list.
|
||||
* @param {string} inText Text to replace banned words in
|
||||
* @returns {string} Text without the "banned" macro
|
||||
*/
|
||||
function bannedWordsReplace(inText) {
|
||||
if (!inText) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const banPattern = /{{banned "(.*)"}}/gi;
|
||||
|
||||
if (main_api == 'textgenerationwebui') {
|
||||
const bans = inText.matchAll(banPattern);
|
||||
if (bans) {
|
||||
for (const banCase of bans) {
|
||||
console.log('Found banned words in macros: ' + banCase[1]);
|
||||
textgenerationwebui_banned_in_macros.push(banCase[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inText = inText.replaceAll(banPattern, '');
|
||||
return inText;
|
||||
}
|
||||
|
||||
function getTimeSinceLastMessage() {
|
||||
const now = moment();
|
||||
|
||||
if (Array.isArray(chat) && chat.length > 0) {
|
||||
let lastMessage;
|
||||
let takeNext = false;
|
||||
|
||||
for (let i = chat.length - 1; i >= 0; i--) {
|
||||
const message = chat[i];
|
||||
|
||||
if (message.is_system) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (message.is_user && takeNext) {
|
||||
lastMessage = message;
|
||||
break;
|
||||
}
|
||||
|
||||
takeNext = true;
|
||||
}
|
||||
|
||||
if (lastMessage?.send_date) {
|
||||
const lastMessageDate = timestampToMoment(lastMessage.send_date);
|
||||
const duration = moment.duration(now.diff(lastMessageDate));
|
||||
return duration.humanize();
|
||||
}
|
||||
}
|
||||
|
||||
return 'just now';
|
||||
}
|
||||
|
||||
function randomReplace(input, emptyListPlaceholder = '') {
|
||||
const randomPatternNew = /{{random\s?::\s?([^}]+)}}/gi;
|
||||
const randomPatternOld = /{{random\s?:\s?([^}]+)}}/gi;
|
||||
|
||||
if (randomPatternNew.test(input)) {
|
||||
return input.replace(randomPatternNew, (match, listString) => {
|
||||
//split on double colons instead of commas to allow for commas inside random items
|
||||
const list = listString.split('::').filter(item => item.length > 0);
|
||||
if (list.length === 0) {
|
||||
return emptyListPlaceholder;
|
||||
}
|
||||
var rng = new Math.seedrandom('added entropy.', { entropy: true });
|
||||
const randomIndex = Math.floor(rng() * list.length);
|
||||
//trim() at the end to allow for empty random values
|
||||
return list[randomIndex].trim();
|
||||
});
|
||||
} else if (randomPatternOld.test(input)) {
|
||||
return input.replace(randomPatternOld, (match, listString) => {
|
||||
const list = listString.split(',').map(item => item.trim()).filter(item => item.length > 0);
|
||||
if (list.length === 0) {
|
||||
return emptyListPlaceholder;
|
||||
}
|
||||
var rng = new Math.seedrandom('added entropy.', { entropy: true });
|
||||
const randomIndex = Math.floor(rng() * list.length);
|
||||
return list[randomIndex];
|
||||
});
|
||||
} else {
|
||||
return input;
|
||||
}
|
||||
}
|
||||
|
||||
function diceRollReplace(input, invalidRollPlaceholder = '') {
|
||||
const rollPattern = /{{roll[ : ]([^}]+)}}/gi;
|
||||
|
||||
return input.replace(rollPattern, (match, matchValue) => {
|
||||
let formula = matchValue.trim();
|
||||
|
||||
if (isDigitsOnly(formula)) {
|
||||
formula = `1d${formula}`;
|
||||
}
|
||||
|
||||
const isValid = droll.validate(formula);
|
||||
|
||||
if (!isValid) {
|
||||
console.debug(`Invalid roll formula: ${formula}`);
|
||||
return invalidRollPlaceholder;
|
||||
}
|
||||
|
||||
const result = droll.roll(formula);
|
||||
return new String(result.total);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Substitutes {{macro}} parameters in a string.
|
||||
* @param {string} content - The string to substitute parameters in.
|
||||
* @param {*} _name1 - The name of the user.
|
||||
* @param {*} _name2 - The name of the character.
|
||||
* @param {*} _original - The original message for {{original}} substitution.
|
||||
* @param {*} _group - The group members list for {{group}} substitution.
|
||||
* @param {boolean} _replaceCharacterCard - Whether to replace character card macros.
|
||||
* @returns {string} The string with substituted parameters.
|
||||
*/
|
||||
export function evaluateMacros(content, _name1, _name2, _original, _group, _replaceCharacterCard = true) {
|
||||
if (!content) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Replace {{original}} with the original message
|
||||
// Note: only replace the first instance of {{original}}
|
||||
// This will hopefully prevent the abuse
|
||||
if (typeof _original === 'string') {
|
||||
content = content.replace(/{{original}}/i, _original);
|
||||
}
|
||||
content = diceRollReplace(content);
|
||||
content = replaceInstructMacros(content);
|
||||
content = replaceVariableMacros(content);
|
||||
content = content.replace(/{{newline}}/gi, '\n');
|
||||
content = content.replace(/{{input}}/gi, String($('#send_textarea').val()));
|
||||
|
||||
if (_replaceCharacterCard) {
|
||||
const fields = getCharacterCardFields();
|
||||
content = content.replace(/{{charPrompt}}/gi, fields.system || '');
|
||||
content = content.replace(/{{charJailbreak}}/gi, fields.jailbreak || '');
|
||||
content = content.replace(/{{description}}/gi, fields.description || '');
|
||||
content = content.replace(/{{personality}}/gi, fields.personality || '');
|
||||
content = content.replace(/{{scenario}}/gi, fields.scenario || '');
|
||||
content = content.replace(/{{persona}}/gi, fields.persona || '');
|
||||
content = content.replace(/{{mesExamples}}/gi, fields.mesExamples || '');
|
||||
}
|
||||
|
||||
content = content.replace(/{{maxPrompt}}/gi, () => String(getMaxContextSize()));
|
||||
content = content.replace(/{{user}}/gi, _name1);
|
||||
content = content.replace(/{{char}}/gi, _name2);
|
||||
content = content.replace(/{{charIfNotGroup}}/gi, _group);
|
||||
content = content.replace(/{{group}}/gi, _group);
|
||||
content = content.replace(/{{lastMessage}}/gi, getLastMessage());
|
||||
content = content.replace(/{{lastMessageId}}/gi, getLastMessageId());
|
||||
content = content.replace(/{{firstIncludedMessageId}}/gi, getFirstIncludedMessageId());
|
||||
content = content.replace(/{{lastSwipeId}}/gi, getLastSwipeId());
|
||||
content = content.replace(/{{currentSwipeId}}/gi, getCurrentSwipeId());
|
||||
|
||||
content = content.replace(/<USER>/gi, _name1);
|
||||
content = content.replace(/<BOT>/gi, _name2);
|
||||
content = content.replace(/<CHARIFNOTGROUP>/gi, _group);
|
||||
content = content.replace(/<GROUP>/gi, _group);
|
||||
|
||||
content = content.replace(/\{\{\/\/([\s\S]*?)\}\}/gm, '');
|
||||
|
||||
content = content.replace(/{{time}}/gi, moment().format('LT'));
|
||||
content = content.replace(/{{date}}/gi, moment().format('LL'));
|
||||
content = content.replace(/{{weekday}}/gi, moment().format('dddd'));
|
||||
content = content.replace(/{{isotime}}/gi, moment().format('HH:mm'));
|
||||
content = content.replace(/{{isodate}}/gi, moment().format('YYYY-MM-DD'));
|
||||
|
||||
content = content.replace(/{{datetimeformat +([^}]*)}}/gi, (_, format) => {
|
||||
const formattedTime = moment().format(format);
|
||||
return formattedTime;
|
||||
});
|
||||
content = content.replace(/{{idle_duration}}/gi, () => getTimeSinceLastMessage());
|
||||
content = content.replace(/{{time_UTC([-+]\d+)}}/gi, (_, offset) => {
|
||||
const utcOffset = parseInt(offset, 10);
|
||||
const utcTime = moment().utc().utcOffset(utcOffset).format('LT');
|
||||
return utcTime;
|
||||
});
|
||||
content = bannedWordsReplace(content);
|
||||
content = randomReplace(content);
|
||||
return content;
|
||||
}
|
@ -62,6 +62,7 @@ import {
|
||||
formatInstructModePrompt,
|
||||
formatInstructModeSystemPrompt,
|
||||
} from './instruct-mode.js';
|
||||
import { isMobile } from './RossAscends-mods.js';
|
||||
|
||||
export {
|
||||
openai_messages_count,
|
||||
@ -187,6 +188,8 @@ const default_settings = {
|
||||
count_pen: 0.0,
|
||||
top_p_openai: 1.0,
|
||||
top_k_openai: 0,
|
||||
min_p_openai: 0,
|
||||
top_a_openai: 1,
|
||||
stream_openai: false,
|
||||
openai_max_context: max_4k,
|
||||
openai_max_tokens: 300,
|
||||
@ -252,6 +255,8 @@ const oai_settings = {
|
||||
count_pen: 0.0,
|
||||
top_p_openai: 1.0,
|
||||
top_k_openai: 0,
|
||||
min_p_openai: 0,
|
||||
top_a_openai: 1,
|
||||
stream_openai: false,
|
||||
openai_max_context: max_4k,
|
||||
openai_max_tokens: 300,
|
||||
@ -1298,6 +1303,25 @@ function getChatCompletionModel() {
|
||||
}
|
||||
}
|
||||
|
||||
function getOpenRouterModelTemplate(option) {
|
||||
const model = model_list.find(x => x.id === option?.element?.value);
|
||||
|
||||
if (!option.id || !model) {
|
||||
return option.text;
|
||||
}
|
||||
|
||||
let tokens_dollar = Number(1 / (1000 * model.pricing?.prompt));
|
||||
let tokens_rounded = (Math.round(tokens_dollar * 1000) / 1000).toFixed(0);
|
||||
|
||||
const price = 0 === Number(model.pricing?.prompt) ? 'Free' : `${tokens_rounded}k t/$ `;
|
||||
|
||||
return $((`
|
||||
<div class="flex-container flexFlowColumn" title="${DOMPurify.sanitize(model.id)}">
|
||||
<div><strong>${DOMPurify.sanitize(model.name)}</strong> | ${model.context_length} ctx | <small>${price}</small></div>
|
||||
</div>
|
||||
`));
|
||||
}
|
||||
|
||||
function calculateOpenRouterCost() {
|
||||
if (oai_settings.chat_completion_source !== chat_completion_sources.OPENROUTER) {
|
||||
return;
|
||||
@ -1321,7 +1345,7 @@ function calculateOpenRouterCost() {
|
||||
}
|
||||
|
||||
function saveModelList(data) {
|
||||
model_list = data.map((model) => ({ id: model.id, context_length: model.context_length, pricing: model.pricing, architecture: model.architecture }));
|
||||
model_list = data.map((model) => ({ ...model }));
|
||||
model_list.sort((a, b) => a?.id && b?.id && a.id.localeCompare(b.id));
|
||||
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER) {
|
||||
@ -1376,16 +1400,10 @@ function appendOpenRouterOptions(model_list, groupModels = false, sort = false)
|
||||
$('#model_openrouter_select').append($('<option>', { value: openrouter_website_model, text: 'Use OpenRouter website setting' }));
|
||||
|
||||
const appendOption = (model, parent = null) => {
|
||||
let tokens_dollar = Number(1 / (1000 * model.pricing?.prompt));
|
||||
let tokens_rounded = (Math.round(tokens_dollar * 1000) / 1000).toFixed(0);
|
||||
|
||||
const price = 0 === Number(model.pricing?.prompt) ? 'Free' : `${tokens_rounded}k t/$ `;
|
||||
|
||||
let model_description = `${model.id} | ${price} | ${model.context_length} ctx`;
|
||||
(parent || $('#model_openrouter_select')).append(
|
||||
$('<option>', {
|
||||
value: model.id,
|
||||
text: model_description,
|
||||
text: model.name,
|
||||
}));
|
||||
};
|
||||
|
||||
@ -1414,7 +1432,7 @@ const openRouterSortBy = (data, property = 'alphabetically') => {
|
||||
return parseFloat(a.pricing.prompt) - parseFloat(b.pricing.prompt);
|
||||
} else {
|
||||
// Alphabetically
|
||||
return a?.id && b?.id && a.id.localeCompare(b.id);
|
||||
return a?.name && b?.name && a.name.localeCompare(b.name);
|
||||
}
|
||||
});
|
||||
};
|
||||
@ -1586,6 +1604,8 @@ async function sendOpenAIRequest(type, messages, signal) {
|
||||
|
||||
if (isOpenRouter) {
|
||||
generate_data['top_k'] = Number(oai_settings.top_k_openai);
|
||||
generate_data['min_p'] = Number(oai_settings.min_p_openai);
|
||||
generate_data['top_a'] = Number(oai_settings.top_a_openai);
|
||||
generate_data['use_fallback'] = oai_settings.openrouter_use_fallback;
|
||||
|
||||
if (isTextCompletion) {
|
||||
@ -1611,7 +1631,7 @@ async function sendOpenAIRequest(type, messages, signal) {
|
||||
}
|
||||
|
||||
if (isMistral) {
|
||||
generate_data['safe_mode'] = false; // already defaults to false, but just incase they change that in the future.
|
||||
generate_data['safe_prompt'] = false; // already defaults to false, but just incase they change that in the future.
|
||||
}
|
||||
|
||||
if (isCustom) {
|
||||
@ -2347,6 +2367,8 @@ function loadOpenAISettings(data, settings) {
|
||||
oai_settings.count_pen = settings.count_pen ?? default_settings.count_pen;
|
||||
oai_settings.top_p_openai = settings.top_p_openai ?? default_settings.top_p_openai;
|
||||
oai_settings.top_k_openai = settings.top_k_openai ?? default_settings.top_k_openai;
|
||||
oai_settings.top_a_openai = settings.top_a_openai ?? default_settings.top_a_openai;
|
||||
oai_settings.min_p_openai = settings.min_p_openai ?? default_settings.min_p_openai;
|
||||
oai_settings.stream_openai = settings.stream_openai ?? default_settings.stream_openai;
|
||||
oai_settings.openai_max_context = settings.openai_max_context ?? default_settings.openai_max_context;
|
||||
oai_settings.openai_max_tokens = settings.openai_max_tokens ?? default_settings.openai_max_tokens;
|
||||
@ -2478,6 +2500,10 @@ function loadOpenAISettings(data, settings) {
|
||||
|
||||
$('#top_k_openai').val(oai_settings.top_k_openai);
|
||||
$('#top_k_counter_openai').val(Number(oai_settings.top_k_openai).toFixed(0));
|
||||
$('#top_a_openai').val(oai_settings.top_a_openai);
|
||||
$('#top_a_counter_openai').val(Number(oai_settings.top_a_openai));
|
||||
$('#min_p_openai').val(oai_settings.min_p_openai);
|
||||
$('#min_p_counter_openai').val(Number(oai_settings.min_p_openai));
|
||||
$('#seed_openai').val(oai_settings.seed);
|
||||
|
||||
if (settings.reverse_proxy !== undefined) oai_settings.reverse_proxy = settings.reverse_proxy;
|
||||
@ -2622,6 +2648,8 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
|
||||
count_penalty: settings.count_pen,
|
||||
top_p: settings.top_p_openai,
|
||||
top_k: settings.top_k_openai,
|
||||
top_a: settings.top_a_openai,
|
||||
min_p: settings.min_p_openai,
|
||||
openai_max_context: settings.openai_max_context,
|
||||
openai_max_tokens: settings.openai_max_tokens,
|
||||
wrap_in_quotes: settings.wrap_in_quotes,
|
||||
@ -2981,6 +3009,8 @@ function onSettingsPresetChange() {
|
||||
count_penalty: ['#count_pen', 'count_pen', false],
|
||||
top_p: ['#top_p_openai', 'top_p_openai', false],
|
||||
top_k: ['#top_k_openai', 'top_k_openai', false],
|
||||
top_a: ['#top_a_openai', 'top_a_openai', false],
|
||||
min_p: ['#min_p_openai', 'min_p_openai', false],
|
||||
max_context_unlocked: ['#oai_max_context_unlocked', 'max_context_unlocked', true],
|
||||
openai_model: ['#model_openai_select', 'openai_model', false],
|
||||
claude_model: ['#model_claude_select', 'claude_model', false],
|
||||
@ -3669,50 +3699,62 @@ $(document).ready(async function () {
|
||||
updateScaleForm();
|
||||
});
|
||||
|
||||
$(document).on('input', '#temp_openai', function () {
|
||||
$('#temp_openai').on('input', function () {
|
||||
oai_settings.temp_openai = Number($(this).val());
|
||||
$('#temp_counter_openai').val(Number($(this).val()).toFixed(2));
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$(document).on('input', '#freq_pen_openai', function () {
|
||||
$('#freq_pen_openai').on('input', function () {
|
||||
oai_settings.freq_pen_openai = Number($(this).val());
|
||||
$('#freq_pen_counter_openai').val(Number($(this).val()).toFixed(2));
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$(document).on('input', '#pres_pen_openai', function () {
|
||||
$('#pres_pen_openai').on('input', function () {
|
||||
oai_settings.pres_pen_openai = Number($(this).val());
|
||||
$('#pres_pen_counter_openai').val(Number($(this).val()).toFixed(2));
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$(document).on('input', '#count_pen', function () {
|
||||
$('#count_pen').on('input', function () {
|
||||
oai_settings.count_pen = Number($(this).val());
|
||||
$('#count_pen_counter').val(Number($(this).val()).toFixed(2));
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$(document).on('input', '#top_p_openai', function () {
|
||||
$('#top_p_openai').on('input', function () {
|
||||
oai_settings.top_p_openai = Number($(this).val());
|
||||
$('#top_p_counter_openai').val(Number($(this).val()).toFixed(2));
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$(document).on('input', '#top_k_openai', function () {
|
||||
$('#top_k_openai').on('input', function () {
|
||||
oai_settings.top_k_openai = Number($(this).val());
|
||||
$('#top_k_counter_openai').val(Number($(this).val()).toFixed(0));
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$(document).on('input', '#openai_max_context', function () {
|
||||
$('#top_a_openai').on('input', function () {
|
||||
oai_settings.top_a_openai = Number($(this).val());
|
||||
$('#top_a_counter_openai').val(Number($(this).val()));
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#min_p_openai').on('input', function () {
|
||||
oai_settings.min_p_openai = Number($(this).val());
|
||||
$('#min_p_counter_openai').val(Number($(this).val()));
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#openai_max_context').on('input', function () {
|
||||
oai_settings.openai_max_context = Number($(this).val());
|
||||
$('#openai_max_context_counter').val(`${$(this).val()}`);
|
||||
calculateOpenRouterCost();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$(document).on('input', '#openai_max_tokens', function () {
|
||||
$('#openai_max_tokens').on('input', function () {
|
||||
oai_settings.openai_max_tokens = Number($(this).val());
|
||||
calculateOpenRouterCost();
|
||||
saveSettingsDebounced();
|
||||
@ -3984,6 +4026,16 @@ $(document).ready(async function () {
|
||||
resetScrollHeight($(this));
|
||||
});
|
||||
|
||||
if (!isMobile()) {
|
||||
$('#model_openrouter_select').select2({
|
||||
placeholder: 'Select a model',
|
||||
searchInputPlaceholder: 'Search models...',
|
||||
searchInputCssClass: 'text_pole',
|
||||
width: '100%',
|
||||
templateResult: getOpenRouterModelTemplate,
|
||||
});
|
||||
}
|
||||
|
||||
$('#api_button_openai').on('click', onConnectButtonClick);
|
||||
$('#openai_reverse_proxy').on('input', onReverseProxyInput);
|
||||
$('#model_openai_select').on('change', onModelChange);
|
||||
|
@ -235,6 +235,8 @@ let power_user = {
|
||||
restore_user_input: true,
|
||||
reduced_motion: false,
|
||||
compact_input_area: true,
|
||||
auto_connect: false,
|
||||
auto_load_chat: false,
|
||||
};
|
||||
|
||||
let themes = [];
|
||||
@ -277,6 +279,8 @@ const storage_keys = {
|
||||
enableLabMode: 'enableLabMode',
|
||||
reduced_motion: 'reduced_motion',
|
||||
compact_input_area: 'compact_input_area',
|
||||
auto_connect_legacy: 'AutoConnectEnabled',
|
||||
auto_load_chat_legacy: 'AutoLoadChatEnabled',
|
||||
};
|
||||
|
||||
const contextControls = [
|
||||
@ -604,6 +608,10 @@ async function CreateZenSliders(elmnt) {
|
||||
sliderID == 'rep_pen_range') {
|
||||
decimals = 0;
|
||||
}
|
||||
if (sliderID == 'min_temp_textgenerationwebui' ||
|
||||
sliderID == 'max_temp_textgenerationwebui') {
|
||||
decimals = 2;
|
||||
}
|
||||
if (sliderID == 'eta_cutoff_textgenerationwebui' ||
|
||||
sliderID == 'epsilon_cutoff_textgenerationwebui') {
|
||||
numSteps = 50;
|
||||
@ -633,7 +641,9 @@ async function CreateZenSliders(elmnt) {
|
||||
}
|
||||
if (sliderID == 'mirostat_eta_textgenerationwebui' ||
|
||||
sliderID == 'penalty_alpha_textgenerationwebui' ||
|
||||
sliderID == 'length_penalty_textgenerationwebui') {
|
||||
sliderID == 'length_penalty_textgenerationwebui' ||
|
||||
sliderID == 'min_temp_textgenerationwebui' ||
|
||||
sliderID == 'max_temp_textgenerationwebui') {
|
||||
numSteps = 50;
|
||||
}
|
||||
//customize off values
|
||||
@ -1377,6 +1387,19 @@ function loadPowerUserSettings(settings, data) {
|
||||
const expandMessageActions = localStorage.getItem(storage_keys.expand_message_actions);
|
||||
const enableZenSliders = localStorage.getItem(storage_keys.enableZenSliders);
|
||||
const enableLabMode = localStorage.getItem(storage_keys.enableLabMode);
|
||||
const autoLoadChat = localStorage.getItem(storage_keys.auto_load_chat_legacy);
|
||||
const autoConnect = localStorage.getItem(storage_keys.auto_connect_legacy);
|
||||
|
||||
if (autoLoadChat) {
|
||||
power_user.auto_load_chat = autoLoadChat === 'true';
|
||||
localStorage.removeItem(storage_keys.auto_load_chat_legacy);
|
||||
}
|
||||
|
||||
if (autoConnect) {
|
||||
power_user.auto_connect = autoConnect === 'true';
|
||||
localStorage.removeItem(storage_keys.auto_connect_legacy);
|
||||
}
|
||||
|
||||
power_user.fast_ui_mode = fastUi === null ? true : fastUi == 'true';
|
||||
power_user.movingUI = movingUI === null ? false : movingUI == 'true';
|
||||
power_user.noShadows = noShadows === null ? false : noShadows == 'true';
|
||||
@ -1504,6 +1527,8 @@ function loadPowerUserSettings(settings, data) {
|
||||
$('#border-color-picker').attr('color', power_user.border_color);
|
||||
$('#ui_mode_select').val(power_user.ui_mode).find(`option[value="${power_user.ui_mode}"]`).attr('selected', true);
|
||||
$('#reduced_motion').prop('checked', power_user.reduced_motion);
|
||||
$('#auto-connect-checkbox').prop('checked', power_user.auto_connect);
|
||||
$('#auto-load-chat-checkbox').prop('checked', power_user.auto_load_chat);
|
||||
|
||||
for (const theme of themes) {
|
||||
const option = document.createElement('option');
|
||||
@ -3199,6 +3224,16 @@ $(document).ready(() => {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#auto-connect-checkbox').on('input', function () {
|
||||
power_user.auto_connect = !!$(this).prop('checked');
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#auto-load-chat-checkbox').on('input', function () {
|
||||
power_user.auto_load_chat = !!$(this).prop('checked');
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$(document).on('click', '#debug_table [data-debug-function]', function () {
|
||||
const functionId = $(this).data('debug-function');
|
||||
const functionRecord = debug_functions.find(f => f.functionId === functionId);
|
||||
|
@ -311,6 +311,8 @@ class PresetManager {
|
||||
'ollama_model',
|
||||
'server_urls',
|
||||
'type',
|
||||
'custom_model',
|
||||
'bypass_status_check',
|
||||
];
|
||||
const settings = Object.assign({}, getSettingsByApiId(this.apiId));
|
||||
|
||||
|
@ -37,7 +37,7 @@ import { getMessageTimeStamp } from './RossAscends-mods.js';
|
||||
import { hideChatMessage, unhideChatMessage } from './chats.js';
|
||||
import { getContext, saveMetadataDebounced } from './extensions.js';
|
||||
import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
|
||||
import { findGroupMemberId, groups, is_group_generating, resetSelectedGroup, saveGroupChat, selected_group } from './group-chats.js';
|
||||
import { findGroupMemberId, groups, is_group_generating, openGroupById, resetSelectedGroup, saveGroupChat, selected_group } from './group-chats.js';
|
||||
import { autoSelectPersona } from './personas.js';
|
||||
import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
|
||||
import { decodeTextTokens, getFriendlyTokenizerName, getTextTokens, getTokenCount } from './tokenizers.js';
|
||||
@ -149,7 +149,7 @@ parser.addCommand('single', setStoryModeCallback, ['story'], ' – sets the mess
|
||||
parser.addCommand('bubble', setBubbleModeCallback, ['bubbles'], ' – sets the message style to bubble chat mode', true, true);
|
||||
parser.addCommand('flat', setFlatModeCallback, ['default'], ' – sets the message style to flat chat mode', true, true);
|
||||
parser.addCommand('continue', continueChatCallback, ['cont'], ' – continues the last message in the chat', true, true);
|
||||
parser.addCommand('go', goToCharacterCallback, ['char'], '<span class="monospace">(name)</span> – opens up a chat with the character by its name', true, true);
|
||||
parser.addCommand('go', goToCharacterCallback, ['char'], '<span class="monospace">(name)</span> – opens up a chat with the character or group by its name', true, true);
|
||||
parser.addCommand('sysgen', generateSystemMessage, [], '<span class="monospace">(prompt)</span> – generates a system message using a specified prompt', true, true);
|
||||
parser.addCommand('ask', askCharacter, [], '<span class="monospace">(prompt)</span> – asks a specified character card a prompt', true, true);
|
||||
parser.addCommand('delname', deleteMessagesByNameCallback, ['cancel'], '<span class="monospace">(name)</span> – deletes all messages attributed to a specified name', true, true);
|
||||
@ -167,7 +167,7 @@ parser.addCommand('peek', peekCallback, [], '<span class="monospace">(message in
|
||||
parser.addCommand('delswipe', deleteSwipeCallback, ['swipedel'], '<span class="monospace">(optional 1-based id)</span> – deletes a swipe from the last chat message. If swipe id not provided - deletes the current swipe.', true, true);
|
||||
parser.addCommand('echo', echoCallback, [], '<span class="monospace">(title=string severity=info/warning/error/success [text])</span> – echoes the text to toast message. Useful for pipes debugging.', true, true);
|
||||
//parser.addCommand('#', (_, value) => '', [], ' – a comment, does nothing, e.g. <tt>/# the next three commands switch variables a and b</tt>', true, true);
|
||||
parser.addCommand('gen', generateCallback, [], '<span class="monospace">(lock=on/off [prompt])</span> – generates text using the provided prompt and passes it to the next command through the pipe, optionally locking user input while generating.', true, true);
|
||||
parser.addCommand('gen', generateCallback, [], '<span class="monospace">(lock=on/off name="System" [prompt])</span> – generates text using the provided prompt and passes it to the next command through the pipe, optionally locking user input while generating and allowing to configure the in-prompt name for instruct mode (default = "System").', true, true);
|
||||
parser.addCommand('genraw', generateRawCallback, [], '<span class="monospace">(lock=on/off [prompt])</span> – generates text using the provided prompt and passes it to the next command through the pipe, optionally locking user input while generating. Does not include chat history or character card. Use instruct=off to skip instruct formatting, e.g. <tt>/genraw instruct=off Why is the sky blue?</tt>. Use stop=... with a JSON-serialized array to add one-time custom stop strings, e.g. <tt>/genraw stop=["\\n"] Say hi</tt>', true, true);
|
||||
parser.addCommand('addswipe', addSwipeCallback, ['swipeadd'], '<span class="monospace">(text)</span> – adds a swipe to the last chat message.', true, true);
|
||||
parser.addCommand('abort', abortCallback, [], ' – aborts the slash command batch execution', true, true);
|
||||
@ -175,7 +175,7 @@ parser.addCommand('fuzzy', fuzzyCallback, [], 'list=["a","b","c"] (search value)
|
||||
parser.addCommand('pass', (_, arg) => arg, ['return'], '<span class="monospace">(text)</span> – passes the text to the next command through the pipe.', true, true);
|
||||
parser.addCommand('delay', delayCallback, ['wait', 'sleep'], '<span class="monospace">(milliseconds)</span> – delays the next command in the pipe by the specified number of milliseconds.', true, true);
|
||||
parser.addCommand('input', inputCallback, ['prompt'], '<span class="monospace">(default="string" large=on/off wide=on/off okButton="string" rows=number [text])</span> – Shows a popup with the provided text and an input field. The default argument is the default value of the input field, and the text argument is the text to display.', true, true);
|
||||
parser.addCommand('run', runCallback, ['call', 'exec'], '<span class="monospace">(QR label)</span> – runs a Quick Reply with the specified name from the current preset.', true, true);
|
||||
parser.addCommand('run', runCallback, ['call', 'exec'], '<span class="monospace">[key1=value key2=value ...] ([qrSet.]qrLabel)</span> – runs a Quick Reply with the specified name from a currently active preset or from another preset, named arguments can be referenced in a QR with {{arg::key}}.', true, true);
|
||||
parser.addCommand('messages', getMessagesCallback, ['message'], '<span class="monospace">(names=off/on [message index or range])</span> – returns the specified message or range of messages as a string.', true, true);
|
||||
parser.addCommand('setinput', setInputCallback, [], '<span class="monospace">(text)</span> – sets the user input to the specified text and passes it to the next command through the pipe.', true, true);
|
||||
parser.addCommand('popup', popupCallback, [], '<span class="monospace">(large=on/off wide=on/off okButton="string" text)</span> – shows a blocking popup with the specified text and buttons. Returns the input value into the pipe or empty string if canceled.', true, true);
|
||||
@ -445,7 +445,7 @@ function getMessagesCallback(args, value) {
|
||||
return messages.join('\n\n');
|
||||
}
|
||||
|
||||
async function runCallback(_, name) {
|
||||
async function runCallback(args, name) {
|
||||
if (!name) {
|
||||
toastr.warning('No name provided for /run command');
|
||||
return '';
|
||||
@ -458,7 +458,7 @@ async function runCallback(_, name) {
|
||||
|
||||
try {
|
||||
name = name.trim();
|
||||
return await window['executeQuickReplyByName'](name);
|
||||
return await window['executeQuickReplyByName'](name, args);
|
||||
} catch (error) {
|
||||
toastr.error(`Error running Quick Reply "${name}": ${error.message}`, 'Error');
|
||||
return '';
|
||||
@ -587,7 +587,8 @@ async function generateCallback(args, value) {
|
||||
}
|
||||
|
||||
setEphemeralStopStrings(resolveVariable(args?.stop));
|
||||
const result = await generateQuietPrompt(value, false, false, '');
|
||||
const name = args?.name;
|
||||
const result = await generateQuietPrompt(value, false, false, '', name);
|
||||
return result;
|
||||
} finally {
|
||||
if (lock) {
|
||||
@ -1135,8 +1136,14 @@ async function goToCharacterCallback(_, name) {
|
||||
await openChat(new String(characterIndex));
|
||||
return characters[characterIndex]?.name;
|
||||
} else {
|
||||
console.warn(`No matches found for name "${name}"`);
|
||||
return '';
|
||||
const group = groups.find(it => it.name.toLowerCase() == name.toLowerCase());
|
||||
if (group) {
|
||||
await openGroupById(group.id);
|
||||
return group.name;
|
||||
} else {
|
||||
console.warn(`No matches found for name "${name}"`);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,7 @@
|
||||
<li><tt>{{scenario}}</tt> – the Character's Scenario</li>
|
||||
<li><tt>{{persona}}</tt> – your current Persona Description</li>
|
||||
<li><tt>{{mesExamples}}</tt> – the Character's Dialogue Examples</li>
|
||||
<li><tt>{{mesExamplesRaw}}</tt> – unformatted Dialogue Examples <b>(only for Story String)</b></li>
|
||||
<li><tt>{{user}}</tt> – your current Persona username</li>
|
||||
<li><tt>{{char}}</tt> – the Character's name</li>
|
||||
<li><tt>{{lastMessage}}</tt> - the text of the latest chat message.</li>
|
||||
|
@ -36,7 +36,7 @@
|
||||
<h3>Still have questions?</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<a target="_blank" href="https://discord.gg/RZdyAEUPvj">
|
||||
<a target="_blank" href="https://discord.gg/sillytavern">
|
||||
Join the SillyTavern Discord
|
||||
</a>
|
||||
</li>
|
||||
|
@ -15,7 +15,7 @@ import {
|
||||
registerDebugFunction,
|
||||
} from './power-user.js';
|
||||
import EventSourceStream from './sse-stream.js';
|
||||
import { SENTENCEPIECE_TOKENIZERS, getTextTokens, tokenizers } from './tokenizers.js';
|
||||
import { SENTENCEPIECE_TOKENIZERS, TEXTGEN_TOKENIZERS, getTextTokens, tokenizers } from './tokenizers.js';
|
||||
import { getSortableDelay, onlyUnique } from './utils.js';
|
||||
|
||||
export {
|
||||
@ -47,7 +47,7 @@ let MANCER_SERVER = localStorage.getItem(MANCER_SERVER_KEY) ?? MANCER_SERVER_DEF
|
||||
let TOGETHERAI_SERVER = 'https://api.together.xyz';
|
||||
|
||||
const SERVER_INPUTS = {
|
||||
[textgen_types.OOBA]: '#textgenerationwebui_api_url_text',
|
||||
[textgen_types.OOBA]: '#textgenerationwebui_api_url_text',
|
||||
[textgen_types.APHRODITE]: '#aphrodite_api_url_text',
|
||||
[textgen_types.TABBY]: '#tabby_api_url_text',
|
||||
[textgen_types.KOBOLDCPP]: '#koboldcpp_api_url_text',
|
||||
@ -79,6 +79,9 @@ const settings = {
|
||||
presence_pen: 0,
|
||||
do_sample: true,
|
||||
early_stopping: false,
|
||||
dynatemp: false,
|
||||
min_temp: 0,
|
||||
max_temp: 2.0,
|
||||
seed: -1,
|
||||
preset: 'Default',
|
||||
add_bos_token: true,
|
||||
@ -110,6 +113,8 @@ const settings = {
|
||||
logit_bias: [],
|
||||
n: 1,
|
||||
server_urls: {},
|
||||
custom_model: '',
|
||||
bypass_status_check: false,
|
||||
};
|
||||
|
||||
export let textgenerationwebui_banned_in_macros = [];
|
||||
@ -135,6 +140,9 @@ const setting_names = [
|
||||
'num_beams',
|
||||
'length_penalty',
|
||||
'min_length',
|
||||
'dynatemp',
|
||||
'min_temp',
|
||||
'max_temp',
|
||||
'encoder_rep_pen',
|
||||
'freq_pen',
|
||||
'presence_pen',
|
||||
@ -163,6 +171,8 @@ const setting_names = [
|
||||
'sampler_order',
|
||||
'n',
|
||||
'logit_bias',
|
||||
'custom_model',
|
||||
'bypass_status_check',
|
||||
];
|
||||
|
||||
export function validateTextGenUrl() {
|
||||
@ -241,6 +251,18 @@ function convertPresets(presets) {
|
||||
return Array.isArray(presets) ? presets.map((p) => JSON.parse(p)) : [];
|
||||
}
|
||||
|
||||
function getTokenizerForTokenIds() {
|
||||
if (power_user.tokenizer === tokenizers.API_CURRENT && TEXTGEN_TOKENIZERS.includes(settings.type)) {
|
||||
return tokenizers.API_CURRENT;
|
||||
}
|
||||
|
||||
if (SENTENCEPIECE_TOKENIZERS.includes(power_user.tokenizer)) {
|
||||
return power_user.tokenizer;
|
||||
}
|
||||
|
||||
return tokenizers.LLAMA;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} String with comma-separated banned token IDs
|
||||
*/
|
||||
@ -249,7 +271,7 @@ function getCustomTokenBans() {
|
||||
return '';
|
||||
}
|
||||
|
||||
const tokenizer = SENTENCEPIECE_TOKENIZERS.includes(power_user.tokenizer) ? power_user.tokenizer : tokenizers.LLAMA;
|
||||
const tokenizer = getTokenizerForTokenIds();
|
||||
const result = [];
|
||||
const sequences = settings.banned_tokens
|
||||
.split('\n')
|
||||
@ -301,7 +323,7 @@ function calculateLogitBias() {
|
||||
return {};
|
||||
}
|
||||
|
||||
const tokenizer = SENTENCEPIECE_TOKENIZERS.includes(power_user.tokenizer) ? power_user.tokenizer : tokenizers.LLAMA;
|
||||
const tokenizer = getTokenizerForTokenIds();
|
||||
const result = {};
|
||||
|
||||
/**
|
||||
@ -374,12 +396,14 @@ function loadTextGenSettings(data, loadedSettings) {
|
||||
displayLogitBias(settings.logit_bias, BIAS_KEY);
|
||||
//this is needed because showTypeSpecificControls() does not handle NOT declarations
|
||||
if (settings.type === textgen_types.APHRODITE) {
|
||||
$('[data-forAphro=False]').each(function () {
|
||||
$('[data-forAphro="False"]').each(function () {
|
||||
$(this).hide();
|
||||
});
|
||||
} else {
|
||||
$('[data-forAphro=False]').each(function () {
|
||||
$(this).show();
|
||||
$('[data-forAphro="False"]').each(function () {
|
||||
if ($(this).css('display') !== 'none') { //if it wasn't already hidden by showTypeSpecificControls
|
||||
$(this).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -434,7 +458,7 @@ jQuery(function () {
|
||||
|
||||
if (settings.type === textgen_types.APHRODITE) {
|
||||
//this is needed because showTypeSpecificControls() does not handle NOT declarations
|
||||
$('[data-forAphro=False]').each(function () {
|
||||
$('[data-forAphro="False"]').each(function () {
|
||||
$(this).hide();
|
||||
});
|
||||
$('#mirostat_mode_textgenerationwebui').attr('step', 2); //Aphro disallows mode 1
|
||||
@ -448,7 +472,7 @@ jQuery(function () {
|
||||
}
|
||||
} else {
|
||||
//this is needed because showTypeSpecificControls() does not handle NOT declarations
|
||||
$('[data-forAphro=False]').each(function () {
|
||||
$('[data-forAphro="False"]').each(function () {
|
||||
$(this).show();
|
||||
});
|
||||
$('#mirostat_mode_textgenerationwebui').attr('step', 1);
|
||||
@ -478,6 +502,63 @@ jQuery(function () {
|
||||
selectPreset(presetName);
|
||||
});
|
||||
|
||||
$('#samplerResetButton').off('click').on('click', function () {
|
||||
const inputs = {
|
||||
'temp_textgenerationwebui': 1,
|
||||
'top_k_textgenerationwebui': 0,
|
||||
'top_p_textgenerationwebui': 1,
|
||||
'min_p_textgenerationwebui': 0,
|
||||
'rep_pen_textgenerationwebui': 1,
|
||||
'rep_pen_range_textgenerationwebui': 0,
|
||||
'dynatemp_textgenerationwebui': false,
|
||||
'seed_textgenerationwebui': -1,
|
||||
'ban_eos_token_textgenerationwebui': false,
|
||||
'do_sample_textgenerationwebui': true,
|
||||
'add_bos_token_textgenerationwebui': true,
|
||||
'temperature_last_textgenerationwebui': true,
|
||||
'skip_special_tokens_textgenerationwebui': true,
|
||||
'top_a_textgenerationwebui': 0,
|
||||
'top_a_counter_textgenerationwebui': 0,
|
||||
'mirostat_mode_textgenerationwebui': 0,
|
||||
'mirostat_tau_textgenerationwebui': 5,
|
||||
'mirostat_eta_textgenerationwebui': 0.1,
|
||||
'tfs_textgenerationwebui': 1,
|
||||
'epsilon_cutoff_textgenerationwebui': 0,
|
||||
'eta_cutoff_textgenerationwebui': 0,
|
||||
'encoder_rep_pen_textgenerationwebui': 1,
|
||||
'freq_pen_textgenerationwebui': 0,
|
||||
'presence_pen_textgenerationwebui': 0,
|
||||
'no_repeat_ngram_size_textgenerationwebui': 0,
|
||||
'min_length_textgenerationwebui': 0,
|
||||
'num_beams_textgenerationwebui': 1,
|
||||
'length_penalty_textgenerationwebui': 0,
|
||||
'penalty_alpha_textgenerationwebui': 0,
|
||||
'typical_p_textgenerationwebui': 1, // Added entry
|
||||
'guidance_scale_textgenerationwebui': 1,
|
||||
};
|
||||
|
||||
for (const [id, value] of Object.entries(inputs)) {
|
||||
const inputElement = $(`#${id}`);
|
||||
if (inputElement.prop('type') === 'checkbox') {
|
||||
inputElement.prop('checked', value);
|
||||
} else if (inputElement.prop('type') === 'number') {
|
||||
inputElement.val(value).trigger('input');
|
||||
} else {
|
||||
inputElement.val(value).trigger('input');
|
||||
if (power_user.enableZenSliders) {
|
||||
let masterElementID = inputElement.prop('id');
|
||||
console.log(masterElementID)
|
||||
let zenSlider = $(`#${masterElementID}_zenslider`).slider();
|
||||
zenSlider.slider('option', 'value', value);
|
||||
zenSlider.slider('option', 'slide')
|
||||
.call(zenSlider, null, {
|
||||
handle: $('.ui-slider-handle', zenSlider), value: value,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (const i of setting_names) {
|
||||
$(`#${i}_textgenerationwebui`).attr('x-setting-id', i);
|
||||
$(document).on('input', `#${i}_textgenerationwebui`, function () {
|
||||
@ -514,7 +595,7 @@ jQuery(function () {
|
||||
|
||||
function showTypeSpecificControls(type) {
|
||||
$('[data-tg-type]').each(function () {
|
||||
const tgTypes = $(this).attr('data-tg-type').split(',');
|
||||
const tgTypes = $(this).attr('data-tg-type').split(',').map(x => x.trim());
|
||||
for (const tgType of tgTypes) {
|
||||
if (tgType === type || tgType == 'all') {
|
||||
$(this).show();
|
||||
@ -653,6 +734,10 @@ function toIntArray(string) {
|
||||
}
|
||||
|
||||
function getModel() {
|
||||
if (settings.type === OOBA && settings.custom_model) {
|
||||
return settings.custom_model;
|
||||
}
|
||||
|
||||
if (settings.type === MANCER) {
|
||||
return settings.mancer_model;
|
||||
}
|
||||
@ -684,7 +769,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
|
||||
'model': getModel(),
|
||||
'max_new_tokens': maxTokens,
|
||||
'max_tokens': maxTokens,
|
||||
'temperature': settings.temp,
|
||||
'temperature': settings.dynatemp ? (settings.min_temp + settings.max_temp) / 2 : settings.temp,
|
||||
'top_p': settings.top_p,
|
||||
'typical_p': settings.typical_p,
|
||||
'min_p': settings.min_p,
|
||||
@ -692,12 +777,16 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
|
||||
'frequency_penalty': settings.freq_pen,
|
||||
'presence_penalty': settings.presence_pen,
|
||||
'top_k': settings.top_k,
|
||||
'min_length': settings.min_length,
|
||||
'min_length': settings.type === OOBA ? settings.min_length : undefined,
|
||||
'min_tokens': settings.min_length,
|
||||
'num_beams': settings.num_beams,
|
||||
'num_beams': settings.type === OOBA ? settings.num_beams : undefined,
|
||||
'length_penalty': settings.length_penalty,
|
||||
'early_stopping': settings.early_stopping,
|
||||
'add_bos_token': settings.add_bos_token,
|
||||
'dynamic_temperature': settings.dynatemp,
|
||||
'dynatemp_low': settings.min_temp,
|
||||
'dynatemp_high': settings.max_temp,
|
||||
'dynatemp_range': settings.dynatemp ? (settings.max_temp - settings.min_temp) / 2 : 0,
|
||||
'stopping_strings': getStoppingStrings(isImpersonate, isContinue),
|
||||
'stop': getStoppingStrings(isImpersonate, isContinue),
|
||||
'truncation_length': max_context,
|
||||
@ -705,8 +794,8 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
|
||||
'skip_special_tokens': settings.skip_special_tokens,
|
||||
'top_a': settings.top_a,
|
||||
'tfs': settings.tfs,
|
||||
'epsilon_cutoff': settings.epsilon_cutoff,
|
||||
'eta_cutoff': settings.eta_cutoff,
|
||||
'epsilon_cutoff': settings.type === OOBA ? settings.epsilon_cutoff : undefined,
|
||||
'eta_cutoff': settings.type === OOBA ? settings.eta_cutoff : undefined,
|
||||
'mirostat_mode': settings.mirostat_mode,
|
||||
'mirostat_tau': settings.mirostat_tau,
|
||||
'mirostat_eta': settings.mirostat_eta,
|
||||
@ -719,12 +808,14 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
|
||||
'sampler_order': settings.type === textgen_types.KOBOLDCPP ? settings.sampler_order : undefined,
|
||||
};
|
||||
const nonAphroditeParams = {
|
||||
'rep_pen': settings.rep_pen,
|
||||
'rep_pen_range': settings.rep_pen_range,
|
||||
'repetition_penalty_range': settings.rep_pen_range,
|
||||
'encoder_repetition_penalty': settings.encoder_rep_pen,
|
||||
'no_repeat_ngram_size': settings.no_repeat_ngram_size,
|
||||
'penalty_alpha': settings.penalty_alpha,
|
||||
'temperature_last': settings.temperature_last,
|
||||
'do_sample': settings.do_sample,
|
||||
'encoder_repetition_penalty': settings.type === OOBA ? settings.encoder_rep_pen : undefined,
|
||||
'no_repeat_ngram_size': settings.type === OOBA ? settings.no_repeat_ngram_size : undefined,
|
||||
'penalty_alpha': settings.type === OOBA ? settings.penalty_alpha : undefined,
|
||||
'temperature_last': (settings.type === OOBA || settings.type === APHRODITE) ? settings.temperature_last : undefined,
|
||||
'do_sample': settings.type === OOBA ? settings.do_sample : undefined,
|
||||
'seed': settings.seed,
|
||||
'guidance_scale': cfgValues?.guidanceScale?.value ?? settings.guidance_scale ?? 1,
|
||||
'negative_prompt': cfgValues?.negativePrompt ?? substituteParams(settings.negative_prompt) ?? '',
|
||||
@ -733,7 +824,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
|
||||
'repeat_penalty': settings.rep_pen,
|
||||
'tfs_z': settings.tfs,
|
||||
'repeat_last_n': settings.rep_pen_range,
|
||||
'n_predict': settings.maxTokens,
|
||||
'n_predict': maxTokens,
|
||||
'mirostat': settings.mirostat_mode,
|
||||
'ignore_eos': settings.ban_eos_token,
|
||||
};
|
||||
|
@ -35,6 +35,8 @@ export const SENTENCEPIECE_TOKENIZERS = [
|
||||
//tokenizers.NERD2,
|
||||
];
|
||||
|
||||
export const TEXTGEN_TOKENIZERS = [OOBA, TABBY, KOBOLDCPP, LLAMACPP];
|
||||
|
||||
const TOKENIZER_URLS = {
|
||||
[tokenizers.GPT2]: {
|
||||
encode: '/api/tokenizers/gpt2/encode',
|
||||
@ -190,7 +192,7 @@ export function getTokenizerBestMatch(forApi) {
|
||||
// - Tokenizer haven't reported an error previously
|
||||
const hasTokenizerError = sessionStorage.getItem(TOKENIZER_WARNING_KEY);
|
||||
const isConnected = online_status !== 'no_connection';
|
||||
const isTokenizerSupported = [OOBA, TABBY, KOBOLDCPP, LLAMACPP].includes(textgen_settings.type);
|
||||
const isTokenizerSupported = TEXTGEN_TOKENIZERS.includes(textgen_settings.type);
|
||||
|
||||
if (!hasTokenizerError && isConnected) {
|
||||
if (forApi === 'kobold' && kai_flags.can_use_tokenization) {
|
||||
|
@ -360,6 +360,8 @@ function registerWorldInfoSlashCommands() {
|
||||
return '';
|
||||
}
|
||||
|
||||
value = value.replace(/\\([{}|])/g, '$1');
|
||||
|
||||
const data = await loadWorldInfoData(file);
|
||||
|
||||
if (!data || !('entries' in data)) {
|
||||
@ -556,6 +558,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
|
||||
$('#world_popup_name_button').off('click').on('click', nullWorldInfo);
|
||||
$('#world_popup_export').off('click').on('click', nullWorldInfo);
|
||||
$('#world_popup_delete').off('click').on('click', nullWorldInfo);
|
||||
$('#world_duplicate').off('click').on('click', nullWorldInfo);
|
||||
$('#world_popup_entries_list').hide();
|
||||
$('#world_info_pagination').html('');
|
||||
return;
|
||||
@ -693,6 +696,23 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
|
||||
}
|
||||
});
|
||||
|
||||
$('#world_duplicate').off('click').on('click', async () => {
|
||||
const tempName = getFreeWorldName();
|
||||
const finalName = await callPopup('<h3>Create a new World Info?</h3>Enter a name for the new file:', 'input', tempName);
|
||||
|
||||
if (finalName) {
|
||||
await saveWorldInfo(finalName, data, true);
|
||||
await updateWorldInfoList();
|
||||
|
||||
const selectedIndex = world_names.indexOf(finalName);
|
||||
if (selectedIndex !== -1) {
|
||||
$('#world_editor_select').val(selectedIndex).trigger('change');
|
||||
} else {
|
||||
hideWorldEditor();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$('#world_popup_delete').off('click').on('click', async () => {
|
||||
const confirmation = await callPopup(`<h3>Delete the World/Lorebook: "${name}"?</h3>This action is irreversible!`, 'confirm');
|
||||
|
||||
@ -757,6 +777,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
|
||||
const originalDataKeyMap = {
|
||||
'displayIndex': 'extensions.display_index',
|
||||
'excludeRecursion': 'extensions.exclude_recursion',
|
||||
'preventRecursion': 'extensions.prevent_recursion',
|
||||
'selectiveLogic': 'selectiveLogic',
|
||||
'comment': 'comment',
|
||||
'constant': 'constant',
|
||||
@ -1326,6 +1347,18 @@ function getWorldEntry(name, data, entry) {
|
||||
});
|
||||
excludeRecursionInput.prop('checked', entry.excludeRecursion).trigger('input');
|
||||
|
||||
// prevent recursion
|
||||
const preventRecursionInput = template.find('input[name="prevent_recursion"]');
|
||||
preventRecursionInput.data('uid', entry.uid);
|
||||
preventRecursionInput.on('input', function () {
|
||||
const uid = $(this).data('uid');
|
||||
const value = $(this).prop('checked');
|
||||
data.entries[uid].preventRecursion = value;
|
||||
setOriginalDataValue(data, uid, 'extensions.prevent_recursion', data.entries[uid].preventRecursion);
|
||||
saveWorldInfo(name, data);
|
||||
});
|
||||
preventRecursionInput.prop('checked', entry.preventRecursion).trigger('input');
|
||||
|
||||
// delete button
|
||||
const deleteButton = template.find('.delete_entry_button');
|
||||
deleteButton.data('uid', entry.uid);
|
||||
@ -1883,9 +1916,15 @@ async function checkWorldInfo(chat, maxContext) {
|
||||
needsToScan = false;
|
||||
}
|
||||
|
||||
if (newEntries.length === 0) {
|
||||
console.debug('No new entries activated, stopping');
|
||||
needsToScan = false;
|
||||
}
|
||||
|
||||
if (needsToScan) {
|
||||
const text = newEntries
|
||||
.filter(x => !failedProbabilityChecks.has(x))
|
||||
.filter(x => !x.preventRecursion)
|
||||
.map(x => x.content).join('\n');
|
||||
const currentlyActivatedText = transformString(text);
|
||||
textToScan = (currentlyActivatedText + '\n' + textToScan);
|
||||
@ -1983,13 +2022,17 @@ function filterByInclusionGroups(newEntries, allActivatedEntries) {
|
||||
for (const [key, group] of Object.entries(grouped)) {
|
||||
console.debug(`Checking inclusion group '${key}' with ${group.length} entries`, group);
|
||||
|
||||
if (!Array.isArray(group) || group.length <= 1) {
|
||||
console.debug('Skipping inclusion group check, only one entry');
|
||||
if (Array.from(allActivatedEntries).some(x => x.group === key)) {
|
||||
console.debug(`Skipping inclusion group check, group already activated '${key}'`);
|
||||
// We need to forcefully deactivate all other entries in the group
|
||||
for (const entry of group) {
|
||||
newEntries.splice(newEntries.indexOf(entry), 1);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Array.from(allActivatedEntries).some(x => x.group === key)) {
|
||||
console.debug(`Skipping inclusion group check, group already activated '${key}'`);
|
||||
if (!Array.isArray(group) || group.length <= 1) {
|
||||
console.debug('Skipping inclusion group check, only one entry');
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -2158,6 +2201,7 @@ function convertCharacterBook(characterBook) {
|
||||
order: entry.insertion_order,
|
||||
position: entry.extensions?.position ?? (entry.position === 'before_char' ? world_info_position.before : world_info_position.after),
|
||||
excludeRecursion: entry.extensions?.exclude_recursion ?? false,
|
||||
preventRecursion: entry.extensions?.prevent_recursion ?? false,
|
||||
disable: !entry.enabled,
|
||||
addMemo: entry.comment ? true : false,
|
||||
displayIndex: entry.extensions?.display_index ?? index,
|
||||
@ -2265,24 +2309,52 @@ export async function importEmbeddedWorldInfo(skipPopup = false) {
|
||||
setWorldInfoButtonClass(chid, true);
|
||||
}
|
||||
|
||||
function onWorldInfoChange(_, text) {
|
||||
if (_ !== '__notSlashCommand__') { // if it's a slash command
|
||||
function onWorldInfoChange(args, text) {
|
||||
if (args !== '__notSlashCommand__') { // if it's a slash command
|
||||
const silent = isTrueBoolean(args.silent);
|
||||
if (text.trim() !== '') { // and args are provided
|
||||
const slashInputSplitText = text.trim().toLowerCase().split(',');
|
||||
|
||||
slashInputSplitText.forEach((worldName) => {
|
||||
const wiElement = getWIElement(worldName);
|
||||
if (wiElement.length > 0) {
|
||||
selected_world_info.push(wiElement.text());
|
||||
wiElement.prop('selected', true);
|
||||
toastr.success(`Activated world: ${wiElement.text()}`);
|
||||
const name = wiElement.text();
|
||||
switch (args.state) {
|
||||
case 'off': {
|
||||
if (selected_world_info.includes(name)) {
|
||||
selected_world_info.splice(selected_world_info.indexOf(name), 1);
|
||||
wiElement.prop('selected', false);
|
||||
if (!silent) toastr.success(`Deactivated world: ${name}`);
|
||||
} else {
|
||||
if (!silent) toastr.error(`World was not active: ${name}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'toggle': {
|
||||
if (selected_world_info.includes(name)) {
|
||||
selected_world_info.splice(selected_world_info.indexOf(name), 1);
|
||||
wiElement.prop('selected', false);
|
||||
if (!silent) toastr.success(`Deactivated world: ${name}`);
|
||||
} else {
|
||||
selected_world_info.push(name);
|
||||
wiElement.prop('selected', true);
|
||||
if (!silent) toastr.success(`Activated world: ${name}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
selected_world_info.push(name);
|
||||
wiElement.prop('selected', true);
|
||||
if (!silent) toastr.success(`Activated world: ${name}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
toastr.error(`No world found named: ${worldName}`);
|
||||
if (!silent) toastr.error(`No world found named: ${worldName}`);
|
||||
}
|
||||
});
|
||||
$('#world_info').trigger('change');
|
||||
} else { // if no args, unset all worlds
|
||||
toastr.success('Deactivated all worlds');
|
||||
if (!silent) toastr.success('Deactivated all worlds');
|
||||
selected_world_info = [];
|
||||
$('#world_info').val(null).trigger('change');
|
||||
}
|
||||
@ -2414,7 +2486,7 @@ function assignLorebookToChat() {
|
||||
jQuery(() => {
|
||||
|
||||
$(document).ready(function () {
|
||||
registerSlashCommand('world', onWorldInfoChange, [], '<span class="monospace">(optional name)</span> – sets active World, or unsets if no args provided', true, true);
|
||||
registerSlashCommand('world', onWorldInfoChange, [], '<span class="monospace">[optional state=off|toggle] [optional silent=true] (optional name)</span> – sets active World, or unsets if no args provided, use <code>state=off</code> and <code>state=toggle</code> to deactivate or toggle a World, use <code>silent=true</code> to suppress toast messages', true, true);
|
||||
});
|
||||
|
||||
|
||||
|
13
server.js
13
server.js
@ -294,6 +294,19 @@ app.post('/savequickreply', jsonParser, (request, response) => {
|
||||
return response.sendStatus(200);
|
||||
});
|
||||
|
||||
app.post('/deletequickreply', jsonParser, (request, response) => {
|
||||
if (!request.body || !request.body.name) {
|
||||
return response.sendStatus(400);
|
||||
}
|
||||
|
||||
const filename = path.join(DIRECTORIES.quickreplies, sanitize(request.body.name) + '.json');
|
||||
if (fs.existsSync(filename)) {
|
||||
fs.unlinkSync(filename);
|
||||
}
|
||||
|
||||
return response.sendStatus(200);
|
||||
});
|
||||
|
||||
|
||||
app.post('/uploaduseravatar', urlencodedParser, async (request, response) => {
|
||||
if (!request.file) return response.sendStatus(400);
|
||||
|
@ -334,7 +334,7 @@ async function sendMakerSuiteRequest(request, response) {
|
||||
}
|
||||
|
||||
const responseContent = candidates[0].content ?? candidates[0].output;
|
||||
const responseText = typeof responseContent === 'string' ? responseContent : responseContent.parts?.[0]?.text;
|
||||
const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.[0]?.text;
|
||||
if (!responseText) {
|
||||
let message = 'MakerSuite Candidate text empty';
|
||||
console.log(message, generateResponseJson);
|
||||
@ -483,7 +483,7 @@ async function sendMistralAIRequest(request, response) {
|
||||
'top_p': request.body.top_p,
|
||||
'max_tokens': request.body.max_tokens,
|
||||
'stream': request.body.stream,
|
||||
'safe_mode': request.body.safe_mode,
|
||||
'safe_prompt': request.body.safe_prompt,
|
||||
'random_seed': request.body.seed === -1 ? undefined : request.body.seed,
|
||||
};
|
||||
|
||||
@ -545,6 +545,7 @@ router.post('/status', jsonParser, async function (request, response_getstatus_o
|
||||
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MISTRALAI) {
|
||||
api_url = 'https://api.mistral.ai/v1';
|
||||
api_key_openai = readSecret(SECRET_KEYS.MISTRALAI);
|
||||
headers = {};
|
||||
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.CUSTOM) {
|
||||
api_url = request.body.custom_url;
|
||||
api_key_openai = readSecret(SECRET_KEYS.CUSTOM);
|
||||
@ -721,6 +722,14 @@ router.post('/generate', jsonParser, function (request, response) {
|
||||
headers = { 'HTTP-Referer': request.headers.referer };
|
||||
bodyParams = { 'transforms': ['middle-out'] };
|
||||
|
||||
if (request.body.min_p !== undefined) {
|
||||
bodyParams['min_p'] = request.body.min_p;
|
||||
}
|
||||
|
||||
if (request.body.top_a !== undefined) {
|
||||
bodyParams['top_a'] = request.body.top_a;
|
||||
}
|
||||
|
||||
if (request.body.use_fallback) {
|
||||
bodyParams['route'] = 'fallback';
|
||||
}
|
||||
|
@ -336,7 +336,7 @@ function charaFormatData(data) {
|
||||
|
||||
if (data.world) {
|
||||
try {
|
||||
const file = readWorldInfoFile(data.world);
|
||||
const file = readWorldInfoFile(data.world, false);
|
||||
|
||||
// File was imported - save it to the character book
|
||||
if (file && file.originalData) {
|
||||
@ -387,6 +387,7 @@ function convertWorldInfoToCharacterBook(name, entries) {
|
||||
depth: entry.depth ?? 4,
|
||||
selectiveLogic: entry.selectiveLogic ?? 0,
|
||||
group: entry.group ?? '',
|
||||
prevent_recursion: entry.preventRecursion ?? false,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -10,7 +10,7 @@ const API_NOVELAI = 'https://api.novelai.net';
|
||||
// Ban bracket generation, plus defaults
|
||||
const badWordsList = [
|
||||
[3], [49356], [1431], [31715], [34387], [20765], [30702], [10691], [49333], [1266],
|
||||
[19438], [43145], [26523], [41471], [2936], [85, 85], [49332], [7286], [1115],
|
||||
[19438], [43145], [26523], [41471], [2936], [85, 85], [49332], [7286], [1115], [24],
|
||||
];
|
||||
|
||||
const hypeBotBadWordsList = [
|
||||
@ -175,6 +175,13 @@ router.post('/generate', jsonParser, async function (req, res) {
|
||||
},
|
||||
};
|
||||
|
||||
// Tells the model to stop generation at '>'
|
||||
if ('theme_textadventure' === req.body.prefix &&
|
||||
(true === req.body.model.includes('clio') ||
|
||||
true === req.body.model.includes('kayra'))) {
|
||||
data.parameters.eos_token_id = 49405;
|
||||
}
|
||||
|
||||
console.log(util.inspect(data, { depth: 4 }));
|
||||
|
||||
const args = {
|
||||
|
@ -7,8 +7,14 @@ const writeFileAtomicSync = require('write-file-atomic').sync;
|
||||
const { jsonParser, urlencodedParser } = require('../express-common');
|
||||
const { DIRECTORIES, UPLOADS_PATH } = require('../constants');
|
||||
|
||||
function readWorldInfoFile(worldInfoName) {
|
||||
const dummyObject = { entries: {} };
|
||||
/**
|
||||
* Reads a World Info file and returns its contents
|
||||
* @param {string} worldInfoName Name of the World Info file
|
||||
* @param {boolean} allowDummy If true, returns an empty object if the file doesn't exist
|
||||
* @returns {object} World Info file contents
|
||||
*/
|
||||
function readWorldInfoFile(worldInfoName, allowDummy) {
|
||||
const dummyObject = allowDummy ? { entries: {} } : null;
|
||||
|
||||
if (!worldInfoName) {
|
||||
return dummyObject;
|
||||
@ -34,7 +40,7 @@ router.post('/get', jsonParser, (request, response) => {
|
||||
return response.sendStatus(400);
|
||||
}
|
||||
|
||||
const file = readWorldInfoFile(request.body.name);
|
||||
const file = readWorldInfoFile(request.body.name, true);
|
||||
|
||||
return response.send(file);
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user