Compare commits

..

490 Commits

Author SHA1 Message Date
Cohee
7bf62b3dad Merge pull request #1351 from SillyTavern/staging
Staging
2023-11-13 01:52:46 +02:00
Cohee
25f1afa628 Fix lab mode class 2023-11-13 01:49:36 +02:00
Cohee
e29bcde1d3 Localization hotfix 2023-11-13 01:45:46 +02:00
Cohee
8ff4599e8a Merge branch 'release' into staging 2023-11-12 23:29:13 +02:00
Cohee
6c02a12e88 Proper manual input debounce 2023-11-12 23:28:34 +02:00
Cohee
8fd5a5886b #1347 Localization hotfix 2023-11-12 23:20:32 +02:00
Cohee
31d6c97e70 #1347 Localization hotfix 2023-11-12 23:20:14 +02:00
Cohee
d3e5f6ebc0 #1343 Move bypass check up 2023-11-12 23:08:24 +02:00
Cohee
aeac56c95d Reformat assets.js code 2023-11-12 23:02:07 +02:00
Cohee
2aaaa71d85 Merge branch 'staging' of https://github.com/SillyTavern/SillyTavern into staging 2023-11-12 22:38:55 +02:00
Cohee
500a1dc4c6 Merge pull request #1349 from Tony-sama/staging
update live2d assets detection to allow for subfolder organisation.
2023-11-12 22:38:49 +02:00
Cohee
0d4e5c31e2 Code clean-up 2023-11-12 22:35:17 +02:00
Cohee
9a1d1594d6 Fix formatting in openai.js 2023-11-12 22:14:35 +02:00
Cohee
cd440f6539 Merge pull request #1350 from artisticMink/feature/openrouter-grouping-and-sorting
Feature/openrouter grouping and sorting
2023-11-12 22:13:40 +02:00
Cohee
5bcd49b7ca More concise UI texts, fix button spam clicks 2023-11-12 22:12:16 +02:00
Cohee
2092f849f7 #1348 Show a full screen loader when bulk deleting characters 2023-11-12 21:58:43 +02:00
artisticMink
cc0b4e8174 Access oai_settings instead of dom 2023-11-12 20:55:29 +01:00
Cohee
6e9c6a14f7 Update index.html 2023-11-12 20:35:53 +02:00
Tony Ribeiro
f082420fc7 Use cross os separator for live2d assets path 2023-11-12 19:25:13 +01:00
Tony Ribeiro
c5ea3cfce7 normalize path of live2d assets 2023-11-12 19:21:01 +01:00
Tony Ribeiro
e7e4f75c86 fix new live2d asset listing for character folder. 2023-11-12 19:08:29 +01:00
artisticMink
3bbbf0d8e4 Put openrouter model sorting in drawer
Renames 'Infinity'k tokens to 'Free'
2023-11-12 19:02:41 +01:00
Tony Ribeiro
7f8994c1fd update live2d assets detection to allow for subfolder organisation. 2023-11-12 18:56:01 +01:00
artisticMink
cb2644cdea Add sorting for openrouter models
Alphabetically (default), price, context size
2023-11-12 18:27:56 +01:00
Cohee
dd12cacd16 Fix range validation error 2023-11-12 17:27:40 +02:00
RossAscends
4558f856b5 Merge branch 'staging' of https://github.com/Cohee1207/SillyTavern into staging 2023-11-12 23:57:54 +09:00
RossAscends
5e28d6f651 'off' labels for kobold, typable zenslider handles 2023-11-12 23:57:51 +09:00
artisticMink
a16e34bcef Add optional toggle for grouping openrouter models
By vendor
2023-11-12 15:15:30 +01:00
Cohee
822d9d72ea Force persona creation on avatar uploads. Don't show cropper for images if never resize is enabled. 2023-11-12 15:47:52 +02:00
Cohee
adc533070d Throttle silly animals to one change per minute 2023-11-12 13:26:38 +02:00
Cohee
7afe9e6481 #1343 Add status check bypass 2023-11-12 13:23:46 +02:00
Cohee
efe2a06976 Add null checks for OR pricing 2023-11-12 13:08:22 +02:00
Cohee
4c0b3fb7ae Add null checks for OR pricing 2023-11-12 13:07:57 +02:00
RossAscends
86caffb1c6 Ooba ZenSlider QoL: "Off" for disabled val sampler 2023-11-12 18:22:21 +09:00
RossAscends
583de0d0e7 Merge branch 'staging' of https://github.com/Cohee1207/SillyTavern into staging 2023-11-12 17:23:33 +09:00
RossAscends
986eef9830 fix zenslider and labmode compatibility check 2023-11-12 17:23:29 +09:00
Cohee
35c5d4e528 SD interactive mode fixes 2023-11-12 02:35:37 +02:00
Cohee
6f061adc1e Add OpenAI TTS provider 2023-11-12 02:28:03 +02:00
Cohee
a42c1fc581 Hide MinP under a feature flag. Send Miro to Horde 2023-11-12 01:12:14 +02:00
Cohee
59f7147271 Merge pull request #1342 from ThisIsPIRI/maxcontext
Fix context size counter when size is unlocked
2023-11-12 00:27:24 +02:00
Cohee
879502c1e7 Only allow inlining if OAI is the selected API 2023-11-12 00:13:30 +02:00
Cohee
2c4f53e7b5 Add native GPT-4V image inlining 2023-11-12 00:09:48 +02:00
ThisIsPIRI
ab5b0cb1db Fix context size counter when size is unlocked 2023-11-12 06:32:04 +09:00
Cohee
5d34c8aef5 Merge branch 'staging' of https://github.com/SillyTavern/SillyTavern into staging 2023-11-11 21:53:15 +02:00
Cohee
10264367aa WI editor visual QoL 2023-11-11 21:53:11 +02:00
Cohee
25b549b034 WI custom order (slight return) 2023-11-11 20:16:57 +02:00
Cohee
30f723d9fc Bump package version 2023-11-11 18:26:20 +02:00
Cohee
f79eaa4c8b Merge branch 'staging' of https://github.com/SillyTavern/SillyTavern into staging 2023-11-11 18:25:47 +02:00
Cohee
91a1cc81a0 #1242 Add aux field selector 2023-11-11 18:25:43 +02:00
Cohee
09ebbff30d Merge pull request #1341 from SillyTavern/staging
Staging
2023-11-11 16:51:08 +02:00
Cohee
f1d0e39d39 Require a name for dummy personas 2023-11-11 16:39:54 +02:00
Cohee
8a8880fca1 Visual touch-up 2023-11-11 16:31:13 +02:00
Cohee
28bb5db04f Add new settings to default/settings.json. 2023-11-11 16:21:20 +02:00
Cohee
21fec42172 Merge pull request #1339 from majick/supertemp
Increase kobold max temp to 4.0
2023-11-11 16:14:23 +02:00
Cohee
a02504381a Forbid trigger command while the group is generating 2023-11-11 16:12:02 +02:00
Cohee
303026e01f Nested bogus folders + back button 2023-11-11 14:53:08 +02:00
majick
b445d549db Increase kobold max temp to 4.0
Not only is a higher temp accepted by the back end, but it's just about necessary
in order to make use of Minimum P sampling.
2023-11-10 18:49:53 -08:00
Cohee
5331b5dc8a Fix npm audit 2023-11-10 23:54:44 +02:00
Cohee
7e2ec3ea43 Disable bulk edit for bogus folders 2023-11-10 22:18:48 +02:00
Cohee
066f74ed46 Fix grid view for bogus folders 2023-11-10 22:02:27 +02:00
Cohee
cb1e254cb9 Add tags as folders display mode 2023-11-10 21:56:25 +02:00
Cohee
abe4bb6d54 Add error handling for empty Palm response 2023-11-10 15:55:49 +02:00
Cohee
cfba379777 Hide injection depth input block for relative position 2023-11-10 15:36:42 +02:00
Cohee
0cb9dc2f0b Don't send empty list of bad_word_ids to Novel 2023-11-10 15:07:19 +02:00
Cohee
9a3d239e6d Add error handing for managing extensions 2023-11-10 10:25:34 +02:00
RossAscends
024581de84 dont gaslight user to click zenSlider numbers 2023-11-10 16:31:47 +09:00
RossAscends
dfb8a85eed let loose the Mad Labs 2023-11-10 16:17:38 +09:00
Cohee
2f5e7778cc Don't add items of unknown type to chat completion 2023-11-10 01:08:18 +02:00
Cohee
4e55126606 Replace the link to Turbo tokenizer 2023-11-10 00:57:36 +02:00
Cohee
3cc8d982d5 Move regenerate up in the list 2023-11-10 00:55:39 +02:00
Cohee
bc2b3e9c4e Merge pull request #1337 from artisticMink/hotfix/bulk-edit-enhanced
Fixes for bulk editing overlay
2023-11-10 00:10:27 +02:00
Cohee
3a5cd5d202 Close context menu if open when clicking on group 2023-11-10 00:04:16 +02:00
Cohee
7385de8cf8 Adjust position if context menu is outside of viewport 2023-11-09 23:55:14 +02:00
Cohee
e6fcefd4d1 Fix adding duplicate tags via bulk menu 2023-11-09 23:41:26 +02:00
Cohee
1f89e5a02a Merge branch 'release' into staging 2023-11-09 23:30:12 +02:00
Cohee
eb8cd900fa Merge pull request #1338 from smirgol/bugfix/tts-continue-or-deletion-of-chat-retriggers-tts
fix tts re-trigger for complete/autocomplete/delete
2023-11-09 23:28:17 +02:00
Cohee
34df781699 Make checkboxes brighter than text 2023-11-09 22:50:25 +02:00
Cohee
81fe9aa699 Fix updated tokenization via ooba API 2023-11-09 19:39:08 +02:00
Cohee
3780321ae6 Fix ooba model name displaying as turbo 2023-11-09 19:27:19 +02:00
artisticMink
b2b4be5452 Fix documentation typo 2023-11-09 15:24:24 +01:00
artisticMink
b56fb69eca Prevent deselecting of pointer target character on menu close 2023-11-09 15:18:59 +01:00
artisticMink
2f7e34f66c Close context menu when tapping on character 2023-11-09 15:03:49 +01:00
artisticMink
2174eb8149 Add i18n attributes to context menu 2023-11-09 15:02:56 +01:00
RossAscends
0a409c8c54 Kobold zenSlider, sampler reordering, HTML cleanup 2023-11-09 18:31:44 +09:00
RossAscends
19c5aca404 no more mid-edit swipes 2023-11-09 14:40:14 +09:00
Cohee
d9d61e479d Bypass generation for QR slash commands 2023-11-09 03:19:57 +02:00
Cohee
f2a96427a8 TTS: add slash command to speak 2023-11-09 02:57:40 +02:00
Cohee
ca1c0e35b1 TTS: force speak on first message in new chat 2023-11-09 01:30:54 +02:00
Cohee
0e89bf90bc Use correct tokenizers for logit bias for Mistral and Llama models over OpenRouter 2023-11-09 01:03:54 +02:00
Cohee
1f36fe5193 Fix weird display of macros in help command 2023-11-08 23:56:58 +02:00
Cohee
9aada5837f Fix opening chats in manage chats for export and deletion. 2023-11-08 23:50:25 +02:00
Cohee
9667b82599 Fix /del command (for real this time) 2023-11-08 23:42:44 +02:00
Cohee
aae8707460 Tiny spelling mistake 2023-11-08 23:30:38 +02:00
Cohee
bfbdb9f058 Fix /del command 2023-11-08 23:27:03 +02:00
Cohee
76b8880972 Normalize response statuses 2023-11-08 23:20:55 +02:00
Cohee
6dc5906229 Add ranges to /cut slash command 2023-11-08 23:04:32 +02:00
artisticMink
798099aaaf Prevent context menu from overlapping popup 2023-11-08 21:30:26 +01:00
artisticMink
fa452e94b0 Prevent loss of selected characters when closing menu 2023-11-08 21:22:00 +01:00
artisticMink
b8540a190b Prevent groups from being clicked.
Includes visual indication.
2023-11-08 20:52:34 +01:00
artisticMink
7e3c155e37 Unify access to character block container 2023-11-08 19:29:50 +01:00
smirgol
4f0935c494 better deep clone of message object. re-initalize ttsLastMessage after changing chats 2023-11-08 19:08:42 +01:00
Cohee
606f2bd9bb Allow deleting the first message 2023-11-08 19:55:54 +02:00
artisticMink
b7b478eea8 Cancel long-press on mouseup, touchend
Fixes menu opening multiple times in different position when a user selects quickly and ends with a long-press.
2023-11-08 18:48:35 +01:00
Cohee
480099ee97 Mancer will work in legacy API mode. Remove Soft Prompt mentions. 2023-11-08 18:16:47 +02:00
Cohee
2a5a780f3b Clarify ooba streaming error text 2023-11-08 18:01:41 +02:00
Cohee
3632a01e36 Adjust checkbox text alignment 2023-11-08 17:56:22 +02:00
Cohee
738c1d82c4 Clarify legacy API notification 2023-11-08 17:54:08 +02:00
smirgol
91266c831c fix tts re-trigger for complete/autocomplete/delete
fix re-trigger tts from start if text was added to chat by auto-continue
fix re-trigger tts from start if text was added by continue button
fix re-trigger of tts when message(s) got deleted
2023-11-08 16:40:47 +01:00
Cohee
4f1dbaa34b Remove /v1 from the ooba API URLs 2023-11-08 17:09:33 +02:00
Cohee
d81354e2a5 Merge branch 'staging' of https://github.com/SillyTavern/SillyTavern into staging 2023-11-08 16:29:02 +02:00
Cohee
dbf995fd24 Add character card macros 2023-11-08 16:28:55 +02:00
RossAscends
d8fd4c4767 Merge branch 'staging' of https://github.com/Cohee1207/SillyTavern into staging 2023-11-08 23:24:30 +09:00
RossAscends
a5fd33d08a Kobold sampler restyle 2023-11-08 23:24:28 +09:00
Cohee
d144831569 #1332 Fix group chat branching 2023-11-08 13:26:50 +02:00
Cohee
8f812d6a19 Add more aliases to image slash command 2023-11-08 12:08:13 +02:00
Cohee
740f6548a2 Increase timeouts of OAI out of quota requests 2023-11-08 12:07:14 +02:00
Cohee
b3ced2c4c5 Rename SD extension to "Image Generation" 2023-11-08 10:57:37 +02:00
Cohee
cbc0b41773 Prevent doubled legacy toast 2023-11-08 10:28:02 +02:00
Cohee
b6d29d2980 Return force /api append for Legacy ooba with relaxed URL off 2023-11-08 10:25:49 +02:00
Cohee
e76c18c104 Legacy ooba API compatibility shim 2023-11-08 10:13:28 +02:00
RossAscends
a98c275445 Merge pull request #1335 from bdashore3/staging
Chats: Fix past chat fetching
2023-11-08 14:48:36 +09:00
RossAscends
14ad73872b Merge pull request #1334 from majick/minp-precision
Increase the precision of the Min P slider and counter.
2023-11-08 14:47:00 +09:00
kingbri
8155b1c365 Chats: Fix past chat fetching
Includes checks if an element of the array includes a character ID,
but the array contains characters. We want to check if the value
at the specified index exists in the first place.

Signed-off-by: kingbri <bdashore3@proton.me>
2023-11-08 00:46:26 -05:00
RossAscends
00c029a65a only api format for ooba, more visible checkboxes 2023-11-08 14:08:51 +09:00
majick
24ed0ea186 Increase the precision of the Min P slider and counter.
The most useful values of Min P are very low, between 0.01 and 0.05.
With a precision of 0.1, users have very few usable values to choose
from, while the back end can accept an arbitrary float.
2023-11-07 19:59:32 -08:00
Cohee
865256f5c0 Fix ooba tokenization via API. Fix requiring streaming URL to generate 2023-11-08 03:38:04 +02:00
Cohee
b2629d9718 Refactor status checks and indicators. 2023-11-08 02:52:03 +02:00
Cohee
00a1eaab28 Add timeout for status check 2023-11-08 01:47:31 +02:00
Cohee
44f77f3bb3 Fix mobile scrolling. Increase press timeout. Cancel longpress on scroll 2023-11-08 01:10:51 +02:00
Cohee
48e41d89cb Only override context menu if clicking within character block 2023-11-08 00:59:09 +02:00
Cohee
bda15ef007 Adjust aphro streaming error parser 2023-11-08 00:25:06 +02:00
Cohee
2c7b954a8d #1328 New API schema for ooba / mancer / aphrodite 2023-11-08 00:17:13 +02:00
Cohee
2d2ff5230c Merge pull request #1318 from artisticMink/feature/bulk-edit-enhanced
Feature/bulk edit enhanced
2023-11-07 20:47:06 +02:00
Cohee
0a0382def2 Decrease delay to open bulk edit 2023-11-07 20:46:22 +02:00
Cohee
bea3b5d720 Re-render character list when done editing. Decrease margin to prevent accidental clicks 2023-11-07 20:43:24 +02:00
Cohee
956a80f082 Add rounding for scroll adjustment value 2023-11-07 20:16:33 +02:00
Cohee
94cf4699e6 Fix neo slider z-fighting in Firefox 2023-11-07 17:03:49 +02:00
Cohee
2639548bdb Adjustable caption prompt and template 2023-11-07 01:58:34 +02:00
Cohee
9698c94ee2 #1324 Add captions via OpenAI and Horde 2023-11-07 01:28:46 +02:00
Cohee
2020d12217 Add new GPT 3.5 turbo model 2023-11-07 00:10:32 +02:00
Cohee
2018a6d94a #1325 Increase precision for Novel typical sampler 2023-11-06 23:43:30 +02:00
Cohee
fb5fa8de7c #1325 Increase precision for typical sampler 2023-11-06 23:40:09 +02:00
Cohee
9b0ac48cda Add GPT-4 preview model 2023-11-06 23:29:45 +02:00
Cohee
78e1df28c1 #1325 Increase precision on freq/pres pen for Novel 2023-11-06 23:03:22 +02:00
Cohee
41211137fe #1325 Revert precision changes on some KAI/NAI sliders 2023-11-06 22:58:04 +02:00
Cohee
568f352cf6 [WIP] Placeholder for built-in chat variables 2023-11-06 22:50:32 +02:00
Cohee
1896732f17 Add DALL-E to OpenAI plugin 2023-11-06 21:47:00 +02:00
Cohee
57e845d0d7 Resolve best match tokenizer for itemization. Adjust styles of token counter 2023-11-06 20:25:59 +02:00
Cohee
1a3f100018 Move stop strings post-processing up 2023-11-06 19:19:34 +02:00
artisticMink
dbc7f460e4 Change class name for coherence 2023-11-06 17:20:18 +01:00
artisticMink
a0f828a2da Do not delete selected characters on cancel 2023-11-06 16:13:34 +01:00
artisticMink
581a1e485b Add cursor pointer to characters and groups 2023-11-06 16:13:34 +01:00
artisticMink
6c81cc32e4 Optimization for mobile browsers 2023-11-06 16:13:34 +01:00
artisticMink
3b6e93fc0e Improve visuals in list mode 2023-11-06 16:13:34 +01:00
artisticMink
99ee1b887a Add documentation
Minor refactoring
2023-11-06 16:13:34 +01:00
artisticMink
36cf68a9f4 Sequentially handle persona conversions
Fixes popup overrides.
2023-11-06 16:13:34 +01:00
artisticMink
d2bc1e12c3 Make overlay work with legacy bulk delete button 2023-11-06 16:13:34 +01:00
artisticMink
2d42882a4b Deselect on select state exit 2023-11-06 16:13:34 +01:00
artisticMink
c584da2ea9 Accept both v1 and v2 spec for editcharacterattributes endpoint 2023-11-06 16:13:34 +01:00
artisticMink
42d838a0b3 Refactor TavernCardValidator error property 2023-11-06 16:13:34 +01:00
artisticMink
a876d098fe Skip handleDeleteCharacter when character is undefined 2023-11-06 16:13:34 +01:00
artisticMink
c3ff146dd2 Add bulk tagging 2023-11-06 16:13:34 +01:00
artisticMink
545d933e15 Revert "Add create deck functionality to context menu"
This reverts commit 015c83ae
2023-11-06 16:13:34 +01:00
artisticMink
f1de1d4b77 Restrict longpress start to left mouse button 2023-11-06 16:13:34 +01:00
artisticMink
27bc93936f Tint bulk edit button when bulk edit state is select 2023-11-06 16:13:34 +01:00
artisticMink
778eb2be0e Refactor CharacterGroupOverlay to BulkEditOverlay 2023-11-06 16:13:34 +01:00
artisticMink
e4da7d2a69 Add create deck functionality to context menu 2023-11-06 16:13:34 +01:00
artisticMink
7f6b6615ab Refactor character click handling into method 2023-11-06 16:13:34 +01:00
artisticMink
6eb6527d11 Prototyping 2023-11-06 16:13:34 +01:00
RossAscends
0effbebae7 tokenizer colorization underpinnings (inactive) 2023-11-06 18:04:47 +09:00
Cohee
dc1abed9c6 Fix loader blocking onboarding flow on first run 2023-11-06 10:23:47 +02:00
RossAscends
7a961ca6f6 fix zenslider race condition on F5 value loading 2023-11-06 17:12:47 +09:00
RossAscends
abef26974c tooltipify KAI Ban EOS token hint 2023-11-06 13:45:29 +09:00
RossAscends
8c368bda55 format KAI tooltips 2023-11-06 13:33:38 +09:00
RossAscends
bcc2edd926 Merge pull request #1319 from kalomaze/tooltips-samplers
Tooltips added to KAI Samplers
2023-11-06 13:18:10 +09:00
Cohee
e8ba328a14 Add text chunks display to token counter 2023-11-06 02:42:51 +02:00
Cohee
f248367ca3 Add Mistral tokenizer 2023-11-06 01:26:13 +02:00
RossAscends
c3479b23d9 Merge branch 'staging' of https://github.com/Cohee1207/SillyTavern into staging 2023-11-06 06:54:47 +09:00
RossAscends
37a89d280c restore newbie hides for reorganized ooba settings 2023-11-06 06:54:46 +09:00
Cohee
5c356ab177 Add confirmation to regex deletion 2023-11-05 23:44:28 +02:00
RossAscends
c520ad212a Merge branch 'staging' of https://github.com/Cohee1207/SillyTavern into staging 2023-11-06 06:42:27 +09:00
RossAscends
02b25d080f ooba sliders HTML unified, zensliders optimized 2023-11-06 06:42:13 +09:00
Cohee
4af6955588 Restyle regex script toggles 2023-11-05 23:40:43 +02:00
Cohee
5e88edf22d Merge pull request #1288 from LammyShark/staging
Toggle Regex Script Checkboxes
2023-11-05 23:18:23 +02:00
Cohee
248aec4885 Allow HTML font color overrides
(props to umbrastel)
2023-11-05 22:58:03 +02:00
Cohee
fd9c8b86e9 Token counter layout adjust 2023-11-05 22:55:10 +02:00
Cohee
13d8ec5768 #1317 Rename collapse newlines option 2023-11-05 22:51:16 +02:00
Cohee
f0c0949aa0 Add token ids viewer to tokenizer plugin 2023-11-05 22:45:37 +02:00
Cohee
632d55f6af Merge branch 'staging' of https://github.com/SillyTavern/SillyTavern into staging 2023-11-05 22:03:28 +02:00
Cohee
21e0a42060 Fix arch in models list, remove log 2023-11-05 22:03:20 +02:00
RossAscends
f1eaa18831 Merge branch 'staging' of https://github.com/Cohee1207/SillyTavern into staging 2023-11-06 05:01:17 +09:00
RossAscends
912ea8017f better stepping on ZenSliders amount_gen slider 2023-11-06 05:01:15 +09:00
Cohee
c1e126985d Merge branch 'staging' of https://github.com/SillyTavern/SillyTavern into staging 2023-11-05 21:54:23 +02:00
Cohee
fedc3b887f Add llama2 tokenizer for OpenRouter models 2023-11-05 21:54:19 +02:00
kalomaze
5d77a60939 Kobold tooltips for samplers + temp 2023-11-05 13:47:53 -06:00
Cohee
da4c88f7ec Merge branch 'staging' of https://github.com/SillyTavern/SillyTavern into staging 2023-11-05 19:55:57 +02:00
Cohee
c5e13f3f64 Disable zen sliders on first pull 2023-11-05 19:55:53 +02:00
RossAscends
cf81bd038a Merge branch 'staging' of https://github.com/Cohee1207/SillyTavern into staging 2023-11-06 02:46:21 +09:00
RossAscends
1b8389c8b8 update checkbox and slider styles 2023-11-06 02:46:19 +09:00
Cohee
22ce54649d Cut enc.rep.pen slider name 2023-11-05 19:41:04 +02:00
Cohee
5120cf38f0 Fix macros in first message with alt.greetings 2023-11-05 19:14:17 +02:00
Cohee
583c737106 #1313 Transparency for zoomed avatars 2023-11-05 19:01:48 +02:00
Cohee
ebe5f355d3 Fix formatting of hidden messages 2023-11-05 18:34:11 +02:00
RossAscends
304aa38f24 fix retrieval of currentChatID for renamed chars 2023-11-06 01:23:34 +09:00
RossAscends
a5b5dfdcb6 temperature last for ooba 2023-11-05 23:50:07 +09:00
RossAscends
937c5d7e5f Min P for Ooba, fix preset loading for ZenSliders 2023-11-05 23:08:06 +09:00
RossAscends
19c0100983 finally add theme styles to checkboxes 2023-11-05 15:47:29 +09:00
Cohee
a1b943dbbc Delete itemized prompts on chat deletion 2023-11-05 02:35:44 +02:00
Cohee
88df8501b3 Fix continue on forced OR instruct. Display proper itemized prompt 2023-11-05 02:20:15 +02:00
Cohee
06d5675d6f Fix Novel icon color 2023-11-04 23:57:24 +02:00
Cohee
b15aabf1f8 Merge branch 'staging' of https://github.com/SillyTavern/SillyTavern into staging 2023-11-04 23:25:26 +02:00
Cohee
36c627bd1b Save itemized prompts between sessions 2023-11-04 23:25:22 +02:00
RossAscends
b369be9201 Merge branch 'staging' of https://github.com/Cohee1207/SillyTavern into staging 2023-11-05 05:47:55 +09:00
RossAscends
1c35ff4f3d WIP ZenSliders (ooba only) 2023-11-05 05:47:53 +09:00
Cohee
2f9b247c46 Merge pull request #1307 from aisu-wata0/regexScript_promptOnly
Regex script: prompt only checkbox
2023-11-04 22:37:41 +02:00
Cohee
8183849fe4 Add more aliases for /help pages 2023-11-04 21:35:50 +02:00
Aisu Wata
4bdfd9d164 Regex script: promptOnly modifies coreChat 2023-11-04 15:14:59 -03:00
Cohee
4dccd4a053 Show app loader on start 2023-11-04 20:06:13 +02:00
Cohee
2d3fb08638 Add filter for missing WI entries 2023-11-04 20:02:38 +02:00
Cohee
81e52fb77c Display error when chat info couldn't be loaded 2023-11-04 18:49:06 +02:00
Cohee
9181747939 Update link to sampler parameters doc 2023-11-04 18:18:30 +02:00
Cohee
00cc0483f7 Fix input behavior 2023-11-04 17:48:55 +02:00
Cohee
890ae4bba9 Optimize WI layout 2023-11-04 17:44:43 +02:00
Cohee
b897c8db4a Merge pull request #1303 from aisu-wata0/world_info_minimum
New optional setting: World info: minimum activations
2023-11-04 17:12:58 +02:00
Cohee
e1e472bf79 Add {{lastMessageId}} macro, make async slash command handlers wait for completion 2023-11-04 13:33:09 +02:00
Cohee
b3669afea3 Put /help command on top 2023-11-04 12:37:13 +02:00
RossAscends
0bc6f369b8 Merge branch 'staging' of https://github.com/Cohee1207/SillyTavern into staging 2023-11-04 11:16:14 +09:00
RossAscends
447c43c9d7 placeholders for Aphro samplers 2023-11-04 11:16:12 +09:00
Cohee
6067b2f913 Add autocomplete for slash commands 2023-11-04 01:21:20 +02:00
Aisu Wata
4ec40d575e Regex Scritpt: Only Format Display: fixed typo on subtitle 2023-11-03 18:55:27 -03:00
Cohee
eab25c73e7 Hide/unhide messages 2023-11-03 23:45:56 +02:00
Aisu Wata
c94962aa3c Regex script: promptOnly checkbox and feature 2023-11-02 19:52:33 -03:00
Cohee
f10833a516 Add prompt format override for OpenRouter 2023-11-03 00:34:22 +02:00
Cohee
814c62cc21 Merge branch 'staging' of https://github.com/SillyTavern/SillyTavern into staging 2023-11-02 23:37:25 +02:00
Cohee
35a181e493 Shorten setting names 2023-11-02 23:36:54 +02:00
Cohee
a1df95592c Merge branch 'staging' of https://github.com/SillyTavern/SillyTavern into staging 2023-11-02 20:05:44 +02:00
Cohee
e6ccc53054 BG icon no longer stays in front of moving UI 2023-11-02 20:05:41 +02:00
RossAscends
bb594ebc30 placeholder gui for new example chat parsing 2023-11-02 20:55:56 +09:00
RossAscends
75c6e74d41 support for min P on Kobold API 2023-11-02 14:53:57 +09:00
RossAscends
1966779ade Merge branch 'staging' of https://github.com/Cohee1207/SillyTavern into staging 2023-11-02 13:32:59 +09:00
RossAscends
adef199767 fix slider width on high zoom, restyle ? links 2023-11-02 13:32:57 +09:00
Cohee
c695f73586 Merge branch 'staging' of https://github.com/SillyTavern/SillyTavern into staging 2023-11-01 22:59:07 +02:00
Cohee
e8f4653a25 Add interactive mode for SD plugin 2023-11-01 22:58:59 +02:00
RossAscends
56ec7152d7 better width scaling for mobile Ooba sliders 2023-11-02 03:10:40 +09:00
RossAscends
0576262bb9 Merge branch 'staging' of https://github.com/Cohee1207/SillyTavern into staging 2023-11-02 02:53:53 +09:00
RossAscends
230ef417b6 Ooba sampler panel overhaul 2023-11-02 02:53:50 +09:00
Aisu Wata
3eaff02c2f World Info Min Activations: min max depth default unlimited 2023-11-01 14:36:20 -03:00
Aisu Wata
8533cff188 World info Min Activations: changed UI text 2023-11-01 14:17:30 -03:00
Aisu Wata
39e4a1cd99 World info Min Activations: fewer UI text 2023-11-01 14:15:15 -03:00
Aisu Wata
77bde48a48 World Info Min Activations: added UI & persistence & budget respect 2023-11-01 14:02:38 -03:00
Cohee
e9b8e5d4e9 Extend length limits on most textareas 2023-11-01 10:40:22 +02:00
RossAscends
610c9b9f24 update loader styling 2023-11-01 12:40:21 +09:00
Cohee
a0706fcfc8 Add loader UI component 2023-10-31 22:16:33 +02:00
Cohee
eb4e90c589 Save auto-input to QR 2023-10-31 12:27:40 +02:00
Aisu Wata
8203ebb835 world_info_min_activations 2023-10-30 18:55:32 -03:00
Cohee
d541558f15 Rearrange extensions panel 2023-10-30 21:47:32 +02:00
Cohee
43b06d7df5 Rename view past chats => manage chat files 2023-10-30 21:42:55 +02:00
Cohee
064d331110 Merge pull request #1301 from zgce/release
fix the deeplx for chinese parameter
2023-10-30 16:36:57 +02:00
yuxiang
0921cd6b9c fix the Chinese translation parameter error of deeplx 2023-10-30 22:31:52 +08:00
yuxiang
de77abe152 Merge branch 'release' of https://github.com/SillyTavern/SillyTavern into release
# Conflicts:
#	src/translate.js
2023-10-30 22:10:27 +08:00
yuxiang
31c5036724 Merge branch 'release' of https://github.com/SillyTavern/SillyTavern into release
# Conflicts:
#	src/translate.js
2023-10-30 21:58:10 +08:00
RossAscends
a27e7a139a Chat History Panel Overhaul, shorter filenames 2023-10-30 14:16:20 +09:00
Cohee
2a9250c937 Bump package version 2023-10-30 00:50:25 +02:00
Cohee
1b1f3e9e0c Unbreak CFG sliders 2023-10-29 23:56:25 +02:00
Cohee
68e60ff9ef Unbreak CFG sliders 2023-10-29 23:56:11 +02:00
Cohee
853cf20e6e Remove unreliable check for git installed
It will fail anyway if not installed
2023-10-29 23:46:56 +02:00
Cohee
ea36d34942 Remove unreliable check for git installed
It will fail anyway if not installed
2023-10-29 23:46:13 +02:00
Cohee
91862bff43 Fix double-emit of CHAT_CHANGED on /go command 2023-10-29 23:23:42 +02:00
Cohee
fadd92a6c3 Fix expressions sometimes getting stuck when switching chats 2023-10-29 23:15:40 +02:00
Cohee
9bff2762fc Align message top bar text 2023-10-29 23:15:22 +02:00
Cohee
7b6603614f Add API icons for aphrodite and mancer 2023-10-29 23:02:56 +02:00
Cohee
99e6ee2d4d Slash command to trigger group member response 2023-10-29 19:35:26 +02:00
Cohee
03c3fa24e9 Phone styles adjust 2023-10-29 19:30:56 +02:00
Cohee
483ae22bc3 Add ability to abort generation by extension interceptors 2023-10-29 18:29:10 +02:00
Cohee
9396ca585c #1287 Add user.css file 2023-10-28 12:48:42 +03:00
Cohee
922007ea25 #1294 Fix selective logic for embedded lorebooks 2023-10-28 12:28:03 +03:00
Cohee
97b9d99503 Merge branch 'staging' into release 2023-10-28 03:31:18 +03:00
Cohee
e0be4dde31 Move extensions update check to after init 2023-10-27 21:23:58 +03:00
Cohee
4524f6dbfd Skill issue 2023-10-27 21:09:53 +03:00
Cohee
52ecad1cdf Rework single-line mode, add section for Context Formatting settings 2023-10-27 21:02:03 +03:00
Cohee
9be3645152 Merge pull request #1292 from SillyTavern/staging
Staging
2023-10-27 13:26:43 +03:00
Cohee
a2d8a2a447 Bump package version 2023-10-27 13:23:10 +03:00
Cohee
0ad96b6567 Improve context slider sanity 2023-10-27 13:22:46 +03:00
Cohee
0c36d113bf Add git disclaimer to asset downloader 2023-10-27 13:07:56 +03:00
Cohee
410599b287 Fix unlocked context sliders sanity 2023-10-27 02:01:09 +03:00
Cohee
167b2d0fe4 Add exception handling to stats writer 2023-10-27 01:03:54 +03:00
Cohee
11cc27d9c9 Increase debounce duration for type-in controls to 2sec 2023-10-26 22:02:56 +03:00
Cohee
24f406917d Add seed to Kobold APi 2023-10-26 21:22:00 +03:00
RossAscends
5f64d4be7d fix WI sliders 2023-10-27 03:06:02 +09:00
RossAscends
b29f63f89e fix WI sliders 2023-10-27 03:01:25 +09:00
Cohee
b4c7bb1f7b Merge pull request #1290 from SillyTavern/staging
Staging
2023-10-26 18:44:18 +03:00
Cohee
edbde2be37 Make comment macro multiline 2023-10-26 18:41:35 +03:00
Cohee
1798959ddc Update readme-zh_cn.md 2023-10-26 18:25:51 +03:00
Cohee
d249000b52 Update readme.md 2023-10-26 18:25:10 +03:00
Cohee
58ada40586 Merge pull request #1289 from deffcolony/release
Added new banner for readme.md
2023-10-26 18:18:08 +03:00
deffcolony
f1a13f3093 updated banner 2023-10-26 16:45:03 +02:00
deffcolony
aa3574f15f Added new banner 2023-10-26 15:05:27 +02:00
Cohee
03e513a3e4 Fix slider formatting 2023-10-26 15:17:16 +03:00
Cohee
beca613745 Revert Top P granularity 2023-10-26 15:11:38 +03:00
Cohee
dcca49e848 Revert TFS granularity 2023-10-26 15:09:34 +03:00
Cohee
22cebe3176 Fix settings loading 2023-10-26 15:01:31 +03:00
RossAscends
dd2d292a56 clean up NAI slider decimals 2023-10-26 20:46:09 +09:00
RossAscends
c79bf951b7 minor skill issue with slider overhaul 2023-10-26 20:41:12 +09:00
RossAscends
e9107870c0 Merge branch 'staging' of https://github.com/Cohee1207/SillyTavern into staging 2023-10-26 13:20:49 +09:00
RossAscends
d50124e937 appwide slider overhaul 2023-10-26 13:20:47 +09:00
LammyShark
234baf6276 Toggle Regex Script Checkboxes
Added a checkbox to each Regex Script listed under Saved Scripts to easily enable/disable. It can be cumbersome having to load Regex Editor for each script when disabling/enabling multiple Regex Scripts.
2023-10-25 22:48:19 -04:00
Cohee
339dcaf506 Fix message trigger on disabled character in amalgamate group mode 2023-10-26 02:10:14 +03:00
Cohee
5cdc3d1d18 Smudge groups depth prompts in Join mode. 2023-10-26 00:39:11 +03:00
Cohee
8dcfe57888 #1268 Add group card amalgamation mode 2023-10-26 00:09:22 +03:00
Cohee
2e35cd76ec Merge branch 'staging' of https://github.com/SillyTavern/SillyTavern into staging 2023-10-25 22:39:34 +03:00
Cohee
51d2c9feba Add UI option for group gen strategy 2023-10-25 22:39:31 +03:00
Cohee
864ac3927f Merge pull request #1285 from bdashore3/staging
Add freq pen and presence pen for ooba
2023-10-25 22:38:52 +03:00
Cohee
cb9e334a75 Fix localization strings 2023-10-25 22:36:14 +03:00
kingbri
4f67e9f38b Ooba: Add frequency and presence penalty sliders
These options essentially supersede repetition penalty, but since
ooba also gives the option, add these new sliders in conjunction
with the repetition penalty ones.

More testing is needed to find good default values.

Signed-off-by: kingbri <bdashore3@proton.me>
2023-10-25 15:22:47 -04:00
Cohee
b4a401805f Add explainer for SD prompt expansion 2023-10-25 21:49:32 +03:00
Cohee
f2d5196890 Change scenario override setup method 2023-10-25 19:44:46 +03:00
Cohee
c2ba3a773a Delayed tokenizers initialization 2023-10-25 00:32:49 +03:00
Cohee
3edc456fe7 Extend extensions interop API 2023-10-25 00:28:58 +03:00
Cohee
5dbe2ebf29 Add chat file backups 2023-10-24 22:09:55 +03:00
Cohee
6369ca6483 Merge pull request #1281 from Tony-sama/staging
Live2d assets management
2023-10-24 17:01:17 +03:00
Tony Ribeiro
2b40fc7e76 Use proper path function for live2d asset of character folder 2023-10-24 15:44:40 +02:00
Cohee
52537904f7 Add stopping strings to Horde 2023-10-24 16:23:32 +03:00
Cohee
1bf6d6d9f2 Don't trim the end of instruct last prompt line if newline wrap mode is disabled 2023-10-24 16:02:24 +03:00
Tony Ribeiro
43dae79018 add placeholder in live2d asset folder to have the folder created for users. 2023-10-24 14:47:29 +02:00
Tony Ribeiro
141850eda5 Merge branch 'SillyTavern:staging' into staging 2023-10-24 14:23:45 +02:00
Cohee
affdbb561f Update GPU.ipynb 2023-10-24 13:37:50 +03:00
Cohee
8f118f140f Save settings after selecting persona 2023-10-23 23:22:25 +03:00
Cohee
5b68a438f3 Fix immediate switching between persona namesakes 2023-10-23 23:20:49 +03:00
Cohee
3f9cb7d575 Reduce hr margin 2023-10-23 22:25:17 +03:00
Cohee
85d1a008dd Fix Extras input placeholders 2023-10-23 16:57:40 +03:00
Cohee
f0b20b67de Rearrange ext.panel. Add ext.update notifications. Improve performance on large number of extensions 2023-10-23 16:53:31 +03:00
Cohee
e082138c18 Do shallow clone to speed-up extension installs 2023-10-23 13:27:04 +03:00
Cohee
9a647b96df Add error logging to SD generate endpoint 2023-10-23 13:03:01 +03:00
Cohee
3995238d77 Adjust Anlas guard for NAI Opus 2023-10-23 10:41:39 +03:00
RossAscends
bd9c4d28ca change non-CC API context slider steps to 512 2023-10-23 13:07:04 +09:00
RossAscends
6f79f75f71 remove extra quote mark form bulkdelete html tag 2023-10-23 10:56:46 +09:00
RossAscends
731ebc2eda add toggle for mobile gestures 2023-10-23 10:54:17 +09:00
Cohee
635df947c5 Increase classify sampling window; 300=>500 chars 2023-10-23 02:14:29 +03:00
Cohee
63b34d9851 Call expression module worker when switching chats 2023-10-23 01:56:27 +03:00
Cohee
f9ae7ea949 Add NAI Diffusion v2 model 2023-10-23 01:06:49 +03:00
Cohee
df85218fa7 Merge branch 'release' into staging 2023-10-23 00:27:15 +03:00
Cohee
a4fe78f8ba Merge pull request #1277 from CoryG89/bug-race-condition-tts-autoload-chat
Fix duplicate TTS voicemap UI when autoload last chat is enabled
2023-10-23 00:25:30 +03:00
Tony Ribeiro
4bc2d7f6ac Add management of live2d model in assets folder. 2023-10-22 19:21:10 +02:00
Cory Gross
5b2fff07b8 Fix race condition duplicates TTS UI if autoload chat enabled 2023-10-22 08:54:25 -07:00
Cohee
0517f1bbbc Fix elevenlabs multi-model 2023-10-22 14:46:54 +03:00
Cohee
f49f9c1f96 Merge branch 'release' into staging 2023-10-22 14:01:33 +03:00
Cohee
f9cb6d783e Merge pull request #1263 from bdashore3/preset-expansion
Preset setting expansion
2023-10-22 14:01:06 +03:00
Cohee
600c9c6251 Merge pull request #1274 from CoryG89/bug-tts-autogen-elevenlabs-multilingual 2023-10-22 12:28:35 +03:00
Cory Gross
348805af74 Fix TTS autogen chkbox set by elevenlabs multilingual setting 2023-10-21 22:33:28 -07:00
kingbri
57b299a9cd Presets: Update adventure
Add some default settings and stopping strings.

Signed-off-by: kingbri <bdashore3@proton.me>
2023-10-21 21:58:35 -04:00
Cohee
a662677f06 Add prompt copy button 2023-10-22 00:51:35 +03:00
Cohee
5fa158a37f Add border to enlarged img 2023-10-22 00:43:21 +03:00
Cohee
c49f898886 Display prompts for enlarged images 2023-10-22 00:39:11 +03:00
Cohee
81921bcd77 Saveable SD prefix/negatives 2023-10-22 00:10:48 +03:00
Cohee
18fa925855 Use named constant 2023-10-21 23:33:17 +03:00
Cohee
04a5d9694e Unify preset button styles 2023-10-21 23:32:53 +03:00
Cohee
6296cb8218 Align display name of Summary plugin 2023-10-21 22:20:21 +03:00
Cohee
a4ab898933 Move CFG scale to built-in functionality 2023-10-21 22:19:49 +03:00
Cohee
ed8f5ddc33 Fix BG drawer title position 2023-10-21 21:10:08 +03:00
Cohee
65402eaa23 Move bulk edit to built-in functionality 2023-10-21 21:02:06 +03:00
Cohee
7e116f8b1f Move setting search to built-in functionality 2023-10-21 20:55:51 +03:00
Cohee
999c1b1105 Hide chat BG titles 2023-10-21 20:41:19 +03:00
Cohee
0b535e98b8 Merge branch 'staging' of https://github.com/SillyTavern/SillyTavern into staging 2023-10-21 20:32:39 +03:00
Cohee
4ecf2b9f2d Fix stats init error 2023-10-21 20:32:36 +03:00
Cohee
5012237eb3 Display generated bg in the bg block. Add ability to copy chat bg to system list. 2023-10-21 17:43:25 +03:00
Cohee
8d121bf38f QR menu restyle 2023-10-21 16:17:17 +03:00
Cohee
d0650e6910 Clean-up QR code 2023-10-21 16:09:25 +03:00
IkariDevGIT
1e251c09e3 Better input inject Quick Reply (#1255)
* Force open Char WI with Shift

QoL - Force open character WI selector menu if icon clicked with Shift.

* Update world-info.js (revert personal new WI pos preference)

* Fix element widths

* Fix event typing

* Update index.js

* Update index.js

* change Prompt to Input

this makes it more clear what it does(i think)

---------

Co-authored-by: valden80 <111227649+valden80@users.noreply.github.com>
Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
2023-10-21 16:02:29 +03:00
Cohee
08a1eaad62 Less obnoxious alt.greetings button 2023-10-21 15:54:05 +03:00
Cohee
b4e29bf157 #1272 Optimize performance of context buttons visibility switch 2023-10-21 15:41:27 +03:00
Cohee
97d75aef73 #1272 Fix control buttons when expand actions is used 2023-10-21 15:17:18 +03:00
Cohee
25c461bd3f Add text to open alternate greetings 2023-10-21 15:10:48 +03:00
Cohee
ca73a3a7b7 Merge branch 'release' into staging 2023-10-21 15:06:19 +03:00
Cohee
3816d7b202 Merge pull request #1271 from Xrystallized/faster-load
Faster past chats load
2023-10-21 15:05:53 +03:00
Cohee
008fcece04 Rewrite to sync readdir, add try-catch, filter out invalid files 2023-10-21 15:04:36 +03:00
Cohee
70fa93f0c9 Clarify note macro docs 2023-10-21 14:42:53 +03:00
Cohee
703965aec8 Substitute macro in character editor token counter 2023-10-21 14:39:01 +03:00
IkariDevGIT
6fe4232f75 Add {{// (note)}} macro (#1265)
* add {{# (note)}} macro

* change from # to // for future updates

* fix docs
2023-10-21 14:28:02 +03:00
Cohee
51e0c9130a Merge pull request #1269 from city-unit/feature/slash
Add token counting slash command.
2023-10-21 14:26:45 +03:00
Cohee
85de505553 Move counter to plugin. Use chat context to get messages 2023-10-21 14:23:56 +03:00
Xrystal
1d38109dcf Use JSON instead of json5 2023-10-21 18:56:51 +08:00
Xrystal
6f0f420063 Promisify getallchatsofcharacter 2023-10-21 18:55:52 +08:00
city-unit
63ecca1fe2 Add token counting slash command. 2023-10-21 01:04:16 -04:00
RossAscends
b41bf7cf4e Merge pull request #1267 from city-unit/feature/ui
Minor Fix: Hide extra buttons when you click away
2023-10-21 11:36:40 +09:00
city-unit
95a3021e53 Smooth transition 2023-10-20 22:31:57 -04:00
city-unit
b8939b8ccb Hide extra buttons when you click away 2023-10-20 22:14:36 -04:00
kingbri
8d6eaf3da4 Power User: Fix preset error
The checkbox wasn't being looked up correctly, causing issues when
setting some properties.

Signed-off-by: kingbri <bdashore3@proton.me>
2023-10-20 22:05:03 -04:00
kingbri
18c74ecf09 Power User: Reduce preset settings
Make presets affect less settings that are commonly manually changed
by people after setting one.

Signed-off-by: kingbri <bdashore3@proton.me>
2023-10-20 21:18:48 -04:00
Cohee
f285110773 Merge chat backgrounds plugin with main backgrounds menu 2023-10-21 01:40:58 +03:00
Cohee
7f8df9533b Auto-complete code blocks during streaming 2023-10-20 23:52:23 +03:00
Cohee
e4cb916dff Example messages behavior to 3-state selector 2023-10-20 20:09:31 +03:00
Cohee
0b0b125bca Don't double-expand prompts if reusing 2023-10-20 15:43:55 +03:00
Cohee
c4e6b565a5 Add SD prompt expansion 2023-10-20 15:03:26 +03:00
kingbri
6e4236d5ee Presets: Add power user setting support
Settings such as adding character name, stopping strings, etc can be
preset specific. This change is mainly inspired because people (like myself)
often forget to change these settings after switching a preset, which
can cause weird responses from the model.

Signed-off-by: kingbri <bdashore3@proton.me>
2023-10-20 02:07:45 -04:00
Cohee
5c6343e85e #1259 Add more weight to char.name in fuzzy search 2023-10-19 14:03:38 +03:00
Cohee
50924a0672 Merge branch 'staging' of https://github.com/SillyTavern/SillyTavern into staging 2023-10-19 13:37:38 +03:00
Cohee
b167eb9e22 Add raw token ids support to OAI logit bias. Fix token counting for turbo models 2023-10-19 13:37:08 +03:00
Tony Ribeiro
fae364f079 Merge branch 'staging' of https://github.com/Tony-sama/SillyTavern into staging 2023-10-19 00:36:24 +02:00
Tony Ribeiro
2149bee87f Add handling of multiple live2d asset per character 2023-10-19 00:36:19 +02:00
Tony Ribeiro
08bee074ac Merge branch 'SillyTavern:staging' into staging 2023-10-19 00:35:12 +02:00
Cohee
ec8d30a19d Remember confirm for assets list 2023-10-19 00:26:00 +03:00
Cohee
31242e23eb Merge pull request #1249 from bdashore3/staging
Add ask command
2023-10-18 21:28:20 +03:00
Cohee
9611e31481 Respect trusted worker flag if auto-adjust is enabled 2023-10-18 14:25:05 +03:00
Cohee
5d1fff3df6 Don't add a newline at the end of empty story string 2023-10-18 11:44:45 +03:00
Tony Ribeiro
1dd747a24d Merge branch 'SillyTavern:staging' into staging 2023-10-17 20:18:29 +02:00
Cohee
cc3e27eca1 Merge pull request #1253 from valden80/shift-open-char_wi
Force open char WI selector with Shift-click
2023-10-17 13:56:39 +03:00
Cohee
77f0fe5b80 Fix event typing 2023-10-17 13:55:02 +03:00
Cohee
5c316b50fa Merge branch 'staging' of https://github.com/SillyTavern/SillyTavern into staging 2023-10-17 13:09:53 +03:00
Cohee
e8dd2e2b4d Fix element widths 2023-10-17 13:09:46 +03:00
valden80
81f1bfd23f Update world-info.js (revert personal new WI pos preference) 2023-10-17 03:15:18 +03:00
valden80
819b92a1d5 Force open Char WI with Shift
QoL - Force open character WI selector menu if icon clicked with Shift.
2023-10-17 03:10:57 +03:00
Cohee
eaadb1c5c2 Prevent double insert of chat lore 2023-10-16 23:13:32 +03:00
Cohee
6a1b230c7e #1226 Add chat-bound lorebooks 2023-10-16 23:03:42 +03:00
Cohee
8a3547ecd0 #1248 Fix a server crash on unexpected models response 2023-10-16 20:25:51 +03:00
Cohee
4f458ce93a Fix import of presets with spaces 2023-10-16 20:01:45 +03:00
Cohee
59af85ce1c #1250 Fix multiple card import tags 2023-10-16 11:37:04 +03:00
kingbri
dfa25a1796 Commands: Add ask command
The ask command aims to ask another character about a question or
topic from the current chat. Essentially, the current chat is taken
out and prompted to another character.

This command also requires a few fixes to sprites and prompt creation.

Signed-off-by: kingbri <bdashore3@proton.me>
2023-10-16 02:12:12 -04:00
Cohee
2a16d24760 Increase 0301 padding 2023-10-16 02:21:27 +03:00
Cohee
4657aef306 Improve inline preview of audio assets 2023-10-16 02:09:43 +03:00
Cohee
9ee2c2e9c1 Add asset preview links 2023-10-16 01:39:08 +03:00
Cohee
02dca52948 Add extra padding for 0301 turbo model. 2023-10-16 01:21:14 +03:00
Cohee
5b954a3584 Rename URL validation util function 2023-10-16 00:50:29 +03:00
Cohee
0f15d2d45b Move Idle extension to external repo. 2023-10-16 00:02:37 +03:00
Cohee
6a9f4a57b9 Move dynamic audio to external repository 2023-10-15 23:03:53 +03:00
Cohee
c8dace09b7 Add failsafe condition preventing gens when cfg settings are not found 2023-10-15 22:21:19 +03:00
Cohee
92ddb2b791 Move HypeBot to external repo 2023-10-15 20:03:52 +03:00
Cohee
30c76eb420 Move dice extension to external repository 2023-10-15 19:56:07 +03:00
Cohee
579f43ed1c Move objective extension to external repo 2023-10-15 19:48:12 +03:00
Cohee
6e6e5f4747 Group popout "improvements" 2023-10-15 19:42:23 +03:00
Cohee
421cda2ef0 Move RVC plugin to external repository 2023-10-15 17:27:11 +03:00
Cohee
0a742b867f Move chromadb plugin to external repository 2023-10-15 16:57:37 +03:00
Cohee
82032133f1 Move randomizer plugin files to external repository 2023-10-15 16:23:32 +03:00
Cohee
b4c3985b61 Disable cache of assets loader 2023-10-15 16:16:35 +03:00
Cohee
19363f6cb9 Move variables extension to external repo 2023-10-15 16:08:19 +03:00
Cohee
d1d14bca13 Merge pull request #1245 from majick/tag-field-wtf-logspam
Make v2 spec read logspam at least slightly actionable.  Slightly.
2023-10-15 15:34:14 +03:00
majick
89d7a05ea7 Missed a spot 2023-10-14 20:08:45 -10:00
majick
a9ebd2f80e Make v2 spec read logspam slightly actionable
The existing logspam doesn't even tell the user what char is failing, which means it's
just going to emit every startup, forever, grinding at the poor user's soul, never
letting them know *which* of their 352 cards is causing the spam... day after day after
day.  Until one day, the poor beleaguered user will emit the world's ugliest PR to at
least find the broken card in an attempt kill it.

Ask me how I know.
2023-10-14 17:41:23 -10:00
Cohee
d0637750e7 Add system message collapse for chat comps 2023-10-14 22:05:09 +03:00
Tony Ribeiro
29f045636a Merge branch 'SillyTavern:staging' into staging 2023-10-14 04:35:42 +02:00
Tony Ribeiro
2f3ded734f Added live2d asset category to allowed character assets. 2023-10-14 04:35:09 +02:00
Cohee
658701a943 Merge branches 'staging' and 'staging' of https://github.com/SillyTavern/SillyTavern into staging 2023-10-13 21:58:33 +03:00
Cohee
61f164b5ff Remove ChromaDB from readmes 2023-10-13 21:58:28 +03:00
RossAscends
7f50d84953 put headers on a diet 2023-10-14 01:09:53 +09:00
RossAscends
19ff0fd618 clean up draggables, gallery, and close buttons 2023-10-14 00:29:41 +09:00
Cohee
d82eb373a6 Less obnoxious hr style 2023-10-13 13:11:21 +03:00
Cohee
edb46f480f Merge branch 'staging' of https://github.com/SillyTavern/SillyTavern into staging 2023-10-13 11:42:45 +03:00
Cohee
a1f6220f33 Nicer display of 3rd-party extension assets 2023-10-13 11:42:38 +03:00
Cohee
410f08a317 Merge pull request #1240 from Tony-sama/staging
Removed speech recognition extension (now third party)
2023-10-13 11:42:07 +03:00
Cohee
e9a12b2f4e Merge pull request #1236 from 50h100a/webui_streaming_errmsg
Check for errors on the `stream_end` event
2023-10-13 11:35:56 +03:00
Tony Ribeiro
2997522c52 Removed speech recognition extension (now third party extension download from assets menu). 2023-10-13 01:34:00 +02:00
RossAscends
a87580663e HR divorces theme border color 2023-10-12 21:13:57 +09:00
Cohee
b5d93f477a Merge pull request #1239 from bdashore3/staging
Include AN into WI scanning
2023-10-12 14:19:37 +03:00
kingbri
2c1a6ca67c World Info: Add scanning with AN
Add support for adding extension prompts into WI scanning. Doing this
required adding a method to get extension prompts by name. Now, AN
and depth prompts can be added to world info scanning.

However, since scanning just looks for keys in the chatlog, append
the extension prompts on the top of scanned text.

Signed-off-by: kingbri <bdashore3@proton.me>
2023-10-11 23:50:58 -04:00
Cohee
84098ae933 Fix injection order (again) 2023-10-11 22:56:17 +03:00
Cohee
e2f0162e5a Fix injection order 2023-10-11 22:42:25 +03:00
Cohee
59ae661f62 Fix itemization viewer 2023-10-11 22:09:24 +03:00
Cohee
c4fbc8373d Add raw generate function 2023-10-11 17:56:52 +03:00
Cohee
abb78d1d6b Add at-depth position for custom Prompt Manager prompts 2023-10-11 16:03:36 +03:00
RossAscends
6b5aa9d06e toggle to skip WI&AN insertion in Summary's prompt 2023-10-11 19:44:22 +09:00
50h100a
6e78ec11fb Check for errors on the stream_end event 2023-10-10 18:04:28 -04:00
Cohee
7be3718a36 Fix first.mes continue for Chat Comps 2023-10-10 20:54:09 +03:00
Cohee
2c54627926 Format code 2023-10-10 20:48:21 +03:00
Cohee
9d710801d8 Merge branch 'staging' of https://github.com/SillyTavern/SillyTavern into staging 2023-10-10 20:48:10 +03:00
Cohee
3fceee8f2b Merge pull request #1230 from ThisIsPIRI/worldimport
Add option to use the old embedded WI toast
2023-10-10 20:45:36 +03:00
ThisIsPIRI
3eb8f56b00 Add option to use the old embedded WI toast 2023-10-11 00:08:08 +09:00
kingbri
dbf964e430 Script: Fix greeting message with continue
Add author's note when continuing the first message of a conversation.

Signed-off-by: kingbri <bdashore3@proton.me>
2023-10-10 14:26:41 +03:00
Cohee
3f406dcdf0 Merge pull request #1228 from Tony-sama/staging 2023-10-10 09:41:02 +03:00
Tony Ribeiro
12a37e5342 correct placeholder text. 2023-10-10 01:52:09 +02:00
Tony Ribeiro
47d4561bf8 Merge branch 'SillyTavern:staging' into staging 2023-10-10 01:51:30 +02:00
Tony Ribeiro
93d1a264da Add official blip asset management. 2023-10-10 01:51:09 +02:00
RossAscends
f88f360404 add padding back to summary popout 2023-10-10 05:53:55 +09:00
RossAscends
83f689d9eb Merge branch 'staging' of https://github.com/Cohee1207/SillyTavern into staging 2023-10-10 05:52:08 +09:00
RossAscends
b1769153c7 add background coloring to popped out Summary 2023-10-10 05:52:06 +09:00
Cohee
1239026a8f Auto-update extensions on version change 2023-10-09 23:45:09 +03:00
RossAscends
575e21a1f5 Merge branch 'staging' of https://github.com/Cohee1207/SillyTavern into staging 2023-10-10 04:49:37 +09:00
RossAscends
96ddd5f4e5 draggable popout for Summary 2023-10-10 04:49:35 +09:00
Cohee
15c81749b8 Filter out invalid/broken characters 2023-10-09 19:17:49 +03:00
Cohee
0873d3eaf9 Filter out invalid/broken characters 2023-10-09 19:09:33 +03:00
Cohee
d81151efa2 Fix first message duplication 2023-10-09 18:22:50 +03:00
Cohee
b84e6e07d1 Assets extensions visual touch-up 2023-10-09 17:21:12 +03:00
Cohee
c9f9bcfc86 Merge pull request #1224 from Tony-sama/staging
Minor safety changes to handle blip extension
2023-10-09 12:08:42 +03:00
Tony Ribeiro
6c1ba0aa13 Merge branch 'SillyTavern:staging' into staging 2023-10-09 01:44:16 +02:00
Tony Ribeiro
df4ca46c02 Save chat id between received and rendered event for safety (blip extension allow multiple message received before one is rendered). Added export in translate extension for usage in blip extension. 2023-10-09 01:26:57 +02:00
RossAscends
dac9c091b2 Merge branch 'staging' of https://github.com/Cohee1207/SillyTavern into staging 2023-10-09 08:22:02 +09:00
RossAscends
e29902ed66 '/help slash' hint for how to batch slash commands 2023-10-09 08:22:00 +09:00
Cohee
b3cdfe4fdf Adjust min value of NAI rep.pen.slope 2023-10-08 23:57:49 +03:00
Cohee
d265179f46 Don't crash ST server on invalid streaming URL 2023-10-08 23:42:28 +03:00
Cohee
54d52a2986 Manage extensions via the assets plugin 2023-10-08 23:20:01 +03:00
Cohee
4f80085fa3 Add sanitation of Stable Horde prompts 2023-10-08 22:29:33 +03:00
yuxiang
76507963d7 add bing translate 2023-10-06 19:39:03 +08:00
151 changed files with 10806 additions and 10270 deletions

View File

@@ -1,8 +1,8 @@
[English](readme.md) | 中文
![image](https://github.com/SillyTavern/SillyTavern/assets/18619528/8c41a061-7f72-4d2b-9d54-e6d058209e7b)
![image](https://github.com/SillyTavern/SillyTavern/assets/18619528/c2be4c3f-aada-4f64-87a3-ae35a68b61a4)
移动设备界面友好多种人工智能服务或模型支持KoboldAI/CPP, Horde, NovelAI, Ooba, OpenAI, OpenRouter, Claude, Scale类似 Galgame 的 老 婆 模 式Horde SD文本系统语音生成世界信息Lorebooks可定制的界面自动翻译和比你所需要的更多的 Prompt。附带扩展服务支持文本绘画生成与语音生成和基于向量数据库 ChromaDB 的聊天信息总结。
移动设备界面友好多种人工智能服务或模型支持KoboldAI/CPP, Horde, NovelAI, Ooba, OpenAI, OpenRouter, Claude, Scale类似 Galgame 的 老 婆 模 式Horde SD文本系统语音生成世界信息Lorebooks可定制的界面自动翻译和比你所需要的更多的 Prompt。附带扩展服务支持文本绘画生成与语音生成和基于向量数据库 的聊天信息总结。
基于 TavernAI 1.2.8 的分叉版本
@@ -81,7 +81,6 @@ SillyTavern 支持扩展服务,一些额外的人工智能模块可通过 [Sil
* 在聊天窗口发送图片,并由人工智能解释图片内容
* 文本图像生成5 预设,以及 "自由模式"
* 聊天信息的文字转语音(通过 ElevenLabs、Silero 或操作系统的语音生成)
* ChromaDB 向量数据库,用于更智能的聊天 Prompt
扩展服务的完整功能介绍和使用教程,请参阅 [Docs](https://docs.sillytavern.app/extras/extensions/)。

15
.github/readme.md vendored
View File

@@ -1,15 +1,19 @@
English | [中文](readme-zh_cn.md)
![image](https://github.com/SillyTavern/SillyTavern/assets/18619528/8c41a061-7f72-4d2b-9d54-e6d058209e7b)
![SillyTavern-Banner](https://github.com/SillyTavern/SillyTavern/assets/18619528/c2be4c3f-aada-4f64-87a3-ae35a68b61a4)
Mobile-friendly, Multi-API (KoboldAI/CPP, Horde, NovelAI, Ooba, OpenAI, OpenRouter, Claude, Scale), VN-like Waifu Mode, Horde SD, System TTS, WorldInfo (lorebooks), customizable UI, auto-translate, and more prompt options than you'd ever want or need. Optional Extras server for more SD/TTS options + ChromaDB/Summarize.
Mobile-friendly layout, Multi-API (KoboldAI/CPP, Horde, NovelAI, Ooba, OpenAI, OpenRouter, Claude, Scale), VN-like Waifu Mode, Stable Diffusion, TTS, WorldInfo (lorebooks), customizable UI, auto-translate, and more prompt options than you'd ever want or need + ability to install third-party extensions.
Based on a fork of TavernAI 1.2.8
Based on a fork of [TavernAI](https://github.com/TavernAI/TavernAI) 1.2.8
## Important news!
1. We have created a [Documentation website](https://docs.sillytavern.app/) to answer most of your questions and help you get started.
2. Missing extensions after the update? Since the 1.10.6 release version, most of the previously built-in extensions have been converted to downloadable add-ons. You can download them via the built-in "Download Extensions and Assets" menu in the extensions panel (stacked blocks icon in the top bar).
### Brought to you by Cohee, RossAscends, and the SillyTavern community
NOTE: We have created a [Documentation website](https://docs.sillytavern.app/) to answer most of your questions and help you get started.
### What is SillyTavern or TavernAI?
SillyTavern is a user interface you can install on your computer (and Android phones) that allows you to interact with text generation AIs and chat/roleplay with characters you or the community create.
@@ -80,7 +84,6 @@ SillyTavern has extensibility support, with some additional AI modules hosted vi
* Sending images to chat, and the AI interpreting the content
* Stable Diffusion image generation (5 chat-related presets plus 'free mode')
* Text-to-speech for AI response messages (via ElevenLabs, Silero, or the OS's System TTS)
* ChromaDB vector storage for smarter chat prompt formatting
A full list of included extensions and tutorials on how to use them can be found in the [Docs](https://docs.sillytavern.app/extras/extensions/).

1
.gitignore vendored
View File

@@ -38,3 +38,4 @@ public/assets/
access.log
/vectors/
/cache/
public/css/user.css

View File

@@ -70,7 +70,7 @@
"#@markdown * ckpt/sd15 - base SD 1.5\n",
"#@markdown * stabilityai/stable-diffusion-2-1-base - base SD 2.1\n",
"extras_enable_chromadb = True #@param {type:\"boolean\"}\n",
"#@markdown Enables ChromaDB for Infinity Context plugin\n",
"#@markdown Enables ChromaDB module\n",
"\n",
"import subprocess\n",
"import secrets\n",
@@ -116,7 +116,7 @@
"!git clone https://github.com/Cohee1207/tts_samples\n",
"!npm install -g localtunnel\n",
"!pip install -r requirements-complete.txt\n",
"!pip install tensorflow==2.12\n",
"!pip install tensorflow==2.14\n",
"!pip install colorama\n",
"!pip install Flask-Cors\n",
"!pip install Flask-Compress\n",

View File

@@ -10,6 +10,7 @@ const listen = true; // If true, Can be access from other device or PC. otherwis
const allowKeysExposure = false; // If true, private API keys could be fetched to the frontend.
const skipContentCheck = false; // If true, no new default content will be delivered to you.
const thumbnailsQuality = 95; // Quality of thumbnails. 0-100
const disableChatBackup = false; // Disables the backup of chat logs to the /backups folder
// If true, Allows insecure settings for listen, whitelist, and authentication.
// Change this setting only on "trusted networks". Do not change this value unless you are aware of the issues that can arise from changing this setting and configuring a insecure setting.
@@ -26,6 +27,8 @@ const extras = {
captioningModel: 'Xenova/vit-gpt2-image-captioning',
// Feature extraction model. HuggingFace ID of a model in ONNX format.
embeddingModel: 'Xenova/all-mpnet-base-v2',
// GPT-2 text generation model. HuggingFace ID of a model in ONNX format.
promptExpansionModel: 'Cohee/fooocus_expansion-onnx',
};
// Request overrides for additional headers
@@ -49,4 +52,5 @@ module.exports = {
requestOverrides,
thumbnailsQuality,
extras,
disableChatBackup,
};

View File

@@ -49,7 +49,6 @@
"ban_eos_token": false,
"skip_special_tokens": true,
"streaming": false,
"streaming_url": "ws://127.0.0.1:5005/api/v1/stream",
"mirostat_mode": 0,
"mirostat_tau": 5,
"mirostat_eta": 0.1,
@@ -164,6 +163,8 @@
"custom_stopping_strings_macro": true,
"fuzzy_search": true,
"encode_tags": false,
"enableLabMode": false,
"enableZenSliders": false,
"ui_mode": 1
},
"extension_settings": {
@@ -405,7 +406,6 @@
"typical": 1,
"tfs": 1,
"rep_pen_slope": 0,
"single_line": false,
"streaming_kobold": false,
"sampler_order": [
6,

1
default/user.css Normal file
View File

@@ -0,0 +1 @@
/* Put custom styles here. */

188
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "sillytavern",
"version": "1.10.5",
"version": "1.10.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sillytavern",
"version": "1.10.5",
"version": "1.10.9",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
@@ -756,6 +756,15 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz",
"integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g=="
},
"node_modules/@types/node-fetch": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.9.tgz",
"integrity": "sha512-bQVlnMLFJ2d35DkPNjEPmd9ueO/rh5EiaZt2bhqiSarPjZIuIV6bPQVqcrEyvNo+AfTrRGVazle1tl597w3gfA==",
"dependencies": {
"@types/node": "*",
"form-data": "^4.0.0"
}
},
"node_modules/@types/responselike": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.1.tgz",
@@ -764,6 +773,17 @@
"@types/node": "*"
}
},
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"dependencies": {
"event-target-shim": "^5.0.0"
},
"engines": {
"node": ">=6.5"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -811,6 +831,17 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"node_modules/agentkeepalive": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz",
"integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==",
"dependencies": {
"humanize-ms": "^1.2.1"
},
"engines": {
"node": ">= 8.0.0"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -885,15 +916,20 @@
}
},
"node_modules/axios": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.5.0.tgz",
"integrity": "sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==",
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.1.tgz",
"integrity": "sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g==",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/base-64": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz",
"integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA=="
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -1115,6 +1151,14 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/charenc": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
"integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==",
"engines": {
"node": "*"
}
},
"node_modules/cheerio": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz",
@@ -1351,6 +1395,14 @@
"node": ">=0.8"
}
},
"node_modules/crypt": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
"integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==",
"engines": {
"node": "*"
}
},
"node_modules/csrf-csrf": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/csrf-csrf/-/csrf-csrf-2.2.4.tgz",
@@ -1474,6 +1526,15 @@
"node": ">= 8.11.4"
}
},
"node_modules/digest-fetch": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/digest-fetch/-/digest-fetch-1.3.0.tgz",
"integrity": "sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA==",
"dependencies": {
"base-64": "^0.1.0",
"md5": "^2.3.0"
}
},
"node_modules/dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -1616,6 +1677,14 @@
"node": ">= 0.6"
}
},
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"engines": {
"node": ">=6"
}
},
"node_modules/exif-parser": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz",
@@ -1813,6 +1882,31 @@
"node": ">= 6"
}
},
"node_modules/form-data-encoder": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
"integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="
},
"node_modules/formdata-node": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
"integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
"dependencies": {
"node-domexception": "1.0.0",
"web-streams-polyfill": "4.0.0-beta.3"
},
"engines": {
"node": ">= 12.20"
}
},
"node_modules/formdata-node/node_modules/web-streams-polyfill": {
"version": "4.0.0-beta.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
"integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
"engines": {
"node": ">= 14"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -2141,6 +2235,14 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"node_modules/humanize-ms": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
"integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
"dependencies": {
"ms": "^2.0.0"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -2236,6 +2338,11 @@
"node": ">= 10"
}
},
"node_modules/is-buffer": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
},
"node_modules/is-core-module": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz",
@@ -2515,6 +2622,16 @@
"node": ">=10"
}
},
"node_modules/md5": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz",
"integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==",
"dependencies": {
"charenc": "0.0.2",
"crypt": "0.0.2",
"is-buffer": "~1.1.6"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -2718,6 +2835,24 @@
"node": ">=10"
}
},
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "github",
"url": "https://paypal.me/jimmywarting"
}
],
"engines": {
"node": ">=10.5.0"
}
},
"node_modules/node-fetch": {
"version": "2.6.12",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz",
@@ -2864,20 +2999,30 @@
}
},
"node_modules/openai": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-3.3.0.tgz",
"integrity": "sha512-uqxI/Au+aPRnsaQRe8CojU0eCR7I0mBiKjD3sNMzY6DaC1ZVrc85u98mtJW6voDug8fgGN+DIZmTDxTthxb7dQ==",
"version": "4.17.4",
"resolved": "https://registry.npmjs.org/openai/-/openai-4.17.4.tgz",
"integrity": "sha512-ThRFkl6snLbcAKS58St7N3CaKuI5WdYUvIjPvf4s+8SdymgNtOfzmZcZXVcCefx04oKFnvZJvIcTh3eAFUUhAQ==",
"dependencies": {
"axios": "^0.26.0",
"form-data": "^4.0.0"
"@types/node": "^18.11.18",
"@types/node-fetch": "^2.6.4",
"abort-controller": "^3.0.0",
"agentkeepalive": "^4.2.1",
"digest-fetch": "^1.3.0",
"form-data-encoder": "1.7.2",
"formdata-node": "^4.3.2",
"node-fetch": "^2.6.7",
"web-streams-polyfill": "^3.2.1"
},
"bin": {
"openai": "bin/cli"
}
},
"node_modules/openai/node_modules/axios": {
"version": "0.26.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
"node_modules/openai/node_modules/@types/node": {
"version": "18.18.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.9.tgz",
"integrity": "sha512-0f5klcuImLnG4Qreu9hPj/rEfFq6YRc5n2mAjSsH+ec/mJL+3voBH0+8T7o8RpFjH7ovc+TRsL/c7OYIQsPTfQ==",
"dependencies": {
"follow-redirects": "^1.14.8"
"undici-types": "~5.26.4"
}
},
"node_modules/p-cancelable": {
@@ -4022,6 +4167,11 @@
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
},
"node_modules/universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
@@ -4099,6 +4249,14 @@
"vectra": "bin/vectra.js"
}
},
"node_modules/web-streams-polyfill": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz",
"integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==",
"engines": {
"node": ">= 8"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",

View File

@@ -38,6 +38,9 @@
"overrides": {
"parse-bmfont-xml": {
"xml2js": "^0.5.0"
},
"vectra": {
"openai": "^4.17.0"
}
},
"name": "sillytavern",
@@ -47,7 +50,7 @@
"type": "git",
"url": "https://github.com/SillyTavern/SillyTavern.git"
},
"version": "1.10.5",
"version": "1.10.9",
"scripts": {
"start": "node server.js",
"start-multi": "node server.js --disableCsrf",

View File

@@ -13,6 +13,7 @@ function createDefaultFiles() {
settings: './public/settings.json',
bg_load: './public/css/bg_load.css',
config: './config.conf',
user: './public/css/user.css',
};
for (const file of Object.values(files)) {

View File

@@ -0,0 +1 @@
Put blip audio files here

View File

@@ -0,0 +1 @@
Put live2d model folders here

View File

@@ -1,6 +1,10 @@
{
"story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{personality}}\n{{/if}}{{#if scenario}}{{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}",
"chat_start": "",
"example_separator": "",
"chat_start": "",
"always_force_name2": false,
"trim_sentences": false,
"include_newline": false,
"single_line": true,
"name": "Adventure"
}

View File

@@ -0,0 +1,105 @@
#rm_print_characters_block.group_overlay_mode_select .character_select {
transition: background-color 0.4s ease;
margin-bottom: 1px;
background-color: rgba(170, 170, 170, 0.15);
}
#rm_print_characters_block.group_overlay_mode_select .bogus_folder_select,
#rm_print_characters_block.group_overlay_mode_select .group_select {
cursor: auto;
filter: saturate(0.3);
}
#rm_print_characters_block.group_overlay_mode_select .bogus_folder_select:hover,
#rm_print_characters_block.group_overlay_mode_select .group_select:hover {
background: none;
}
#rm_print_characters_block.group_overlay_mode_select .character_select input.bulk_select_checkbox {
display: none !important;
}
#rm_print_characters_block.group_overlay_mode_select .character_select.character_selected {
background-color: var(--SmartThemeQuoteColor);
}
#rm_print_characters_block.group_overlay_mode_select .character_select .bulk_select_checkbox {
visibility: hidden;
height: 0 !important;
}
#character_context_menu.hidden { display: none; }
#character_context_menu {
position: absolute;
padding: 3px;
z-index: 9998;
background-color: var(--black90a);
border: 1px solid var(--black90a);
border-radius: 10px;
}
#character_context_menu ul li button {
border: 0;
border-bottom-color: currentcolor;
color: var(--SmartThemeQuoteColor);
background-color: transparent;
font-weight: bold;
font-size: 1em;
padding: 0.5em;
border-bottom: 1px dotted var(--SmartThemeQuoteColor);
width: 100%;
cursor: pointer;
}
#character_context_menu ul li button:hover {
background-color: var(--SmartThemeBlurTintColor);
}
#character_context_menu ul li:last-child button {
border-bottom: 0;
}
#character_context_menu ul li #character_context_menu_delete {
color: var(--fullred);
}
#character_context_menu ul {
list-style-type: none;
padding: 0;
margin: 0;
}
#character_context_menu .character_context_menu_separator {
height: 1px;
background-color: var(--SmartThemeBotMesBlurTintColor);
}
#character_context_menu li:hover {
background-color: var(--SmartThemeBotMesBlurTintColor);
}
#bulkEditButton.bulk_edit_overlay_active {
color: var(--golden);
}
#bulk_tag_shadow_popup {
backdrop-filter: blur(calc(var(--SmartThemeBlurStrength) * 2));
-webkit-backdrop-filter: blur(calc(var(--SmartThemeBlurStrength) * 2));
background-color: var(--black30a);
position: absolute;
width: 100%;
height: 100vh;
height: 100svh;
z-index: 9998;
top: 0;
}
#bulk_tag_shadow_popup #bulk_tag_popup {
padding: 1em;
}
#bulk_tag_shadow_popup #bulk_tag_popup #dialogue_popup_controls .menu_button {
width: 100px;
padding: 0.25em;
}

View File

@@ -3,11 +3,6 @@
display: block;
}
#extensions_status {
/* margin-bottom: 10px; */
font-weight: 700;
}
.extensions_block input[type="submit"]:hover {
background-color: green;
}
@@ -103,39 +98,40 @@ input.extension_missing[type="checkbox"] {
}
/** LEFT COLUMN **/
#extensions_settings>.expression_settings {
order: 1;
/* Must be always on top */
#extensions_settings>#assets_ui {
order: -1;
}
#extensions_settings>.background_settings {
#extensions_settings>.expression_settings {
order: 2;
}
#extensions_settings>.sd_settings {
#extensions_settings>.background_settings {
order: 3;
}
#extensions_settings>#tts_settings {
#extensions_settings>.sd_settings {
order: 4;
}
#extensions_settings>#rvc_settings {
#extensions_settings>#tts_settings {
order: 5;
}
#extensions_settings>.objective-settings {
#extensions_settings>#rvc_settings {
order: 6;
}
#extensions_settings>#speech_recognition_settings {
#extensions_settings>.objective-settings {
order: 7;
}
#extensions_settings>#audio_settings {
#extensions_settings>#speech_recognition_settings {
order: 8;
}
#extensions_settings>#assets_ui {
#extensions_settings>#audio_settings {
order: 9;
}

25
public/css/loader.css Normal file
View File

@@ -0,0 +1,25 @@
#loader {
position: fixed;
margin: 0;
padding: 0;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 999999;
width: 100vw;
height: 100vh;
width: 100svw;
height: 100svh;
background-color: var(--SmartThemeBlurTintColor);
/*for some reason the full screen blur does not work on iOS*/
backdrop-filter: blur(30px);
color: var(--SmartThemeBodyColor);
opacity: 1;
}
#load-spinner {
transition: all 300ms ease-out;
opacity: 1;
}

View File

@@ -188,7 +188,8 @@
#showRawPrompt,
#copyPromptToClipboard,
#groupCurrentMemberPopoutButton {
#groupCurrentMemberPopoutButton,
#summaryExtensionPopoutButton {
display: none;
}
@@ -292,7 +293,7 @@
display: none;
}
#bg_menu_content {
.bg_list {
width: unset;
}
}
@@ -444,4 +445,4 @@
#horde_model {
height: unset;
}
}
}

View File

@@ -78,6 +78,7 @@
#rm_group_members:empty {
width: 100%;
padding: 0.5em 0;
}
#rm_group_members:empty::before {
@@ -226,4 +227,5 @@
.group_member .avatar {
flex-shrink: 0;
}
flex-basis: auto;
}

View File

@@ -6,6 +6,12 @@
color: var(--fullred);
}
.highlighted {
color: black;
background-color: yellow;
text-shadow: none !important;
}
.m-t-0 {
margin-top: 0;
}
@@ -56,23 +62,24 @@
.margin-bot-10px,
.marginBot10 {
margin-bottom: 10px;
margin-bottom: 10px !important;
}
.marginTop10 {
margin-top: 10px;
margin-top: 10px !important;
}
.marginBot5 {
margin-bottom: 5px;
margin-bottom: 5px !important;
}
.marginTop5 {
margin-top: 5px;
margin-top: 5px !important;
}
.marginTopBot5 {
margin: 5px 0;
margin-top: 5px !important;
margin-bottom: 5px !important;
}
.margin5 {
@@ -99,10 +106,18 @@
align-items: flex-end !important;
}
.alignItemsBaseline {
align-items: baseline !important;
}
.alignSelfStart {
align-self: start;
}
.gap0 {
gap: 0 !important;
}
.gap3px {
gap: 3px !important;
}
@@ -115,6 +130,14 @@
gap: 10px !important;
}
.gap10h20v {
gap: 10px 20px !important;
}
.gap10h5v {
gap: 5px 10px !important;
}
.wide10pMinFit {
width: 10%;
min-width: fit-content;
@@ -144,6 +167,10 @@
box-shadow: none !important;
}
.height100p {
height: 100%;
}
.height100pSpaceEvenly {
align-content: space-evenly;
height: 100%;
@@ -202,6 +229,26 @@
display: flex;
}
.flexBasis100p {
flex-basis: 100%;
}
.flexBasis50p {
flex-basis: 50%
}
.flexBasis25p {
flex-basis: 25%
}
.flexBasis200px {
flex-basis: 200px
}
.flexBasis48p {
flex-basis: 48%
}
.flex-container {
display: flex;
gap: 5px;
@@ -216,6 +263,14 @@
flex-grow: 1;
}
.flexShrink {
flex-shrink: 1
}
.flexWrap {
flex-wrap: wrap;
}
.flexnowrap {
flex-wrap: nowrap;
}
@@ -294,10 +349,6 @@
flex: 50%;
}
.wide50p {
width: 50% !important;
}
.wide25p {
width: 25%;
}
@@ -381,6 +432,10 @@
display: none;
}
.hoverglow {
transition: opacity 200ms;
}
.hoverglow:hover {
opacity: 1 !important;
cursor: pointer;
@@ -411,6 +466,10 @@ textarea:disabled {
border: 1px solid purple !important;
}
.fontsize120p {
font-size: calc(var(--mainFontSize) * 1.2) !important;
}
.fontsize80p {
font-size: calc(var(--mainFontSize) * 0.8) !important;
}
@@ -449,6 +508,22 @@ textarea:disabled {
gap: 10px;
}
.opacity50p {
opacity: 0.5
}
.opacity1 {
opacity: 1 !important;
}
.circleborder30px {
right: 30px;
top: 10px;
position: absolute;
border: 1px solid var(--SmartThemeBodyColor);
border-radius: 100%;
aspect-ratio: 1 / 1;
height: 30px;
text-align: center;
padding: 5px;
}

View File

@@ -1,3 +1,4 @@
#bulk_tags_div,
#tags_div {
min-width: 0;
}
@@ -12,7 +13,7 @@
.tag_view_item {
display: flex;
flex-direction: row;
align-items: baseline;
align-items: center;
gap: 10px;
margin-bottom: 5px;
}
@@ -86,10 +87,12 @@
align-items: flex-end;
}
#bulkTagsList,
#tagList.tags {
margin: 5px 0;
}
#bulkTagsList,
#tagList .tag {
opacity: 0.6;
}
@@ -138,8 +141,7 @@
filter: brightness(1);
}
.tags_view,
.open_alternate_greetings {
.tags_view {
margin: 0;
aspect-ratio: 1 / 1;
}
@@ -171,4 +173,4 @@
-1px 1px 0px black,
1px -1px 0px black;
opacity: 1;
}
}

View File

@@ -28,6 +28,7 @@ body.charListGrid #rm_print_characters_block {
align-content: flex-start;
}
body.charListGrid #rm_print_characters_block .bogus_folder_select,
body.charListGrid #rm_print_characters_block .character_select {
width: 30%;
align-items: flex-start;
@@ -37,6 +38,7 @@ body.charListGrid #rm_print_characters_block .character_select {
max-width: 100px;
}
body.charListGrid #rm_print_characters_block .bogus_folder_select .ch_name,
body.charListGrid #rm_print_characters_block .character_select .ch_name,
body.charListGrid #rm_print_characters_block .group_select .ch_name {
width: 100%;
@@ -45,10 +47,12 @@ body.charListGrid #rm_print_characters_block .group_select .ch_name {
font-size: calc(var(--mainFontSize) * .8);
}
body.charListGrid #rm_print_characters_block .bogus_folder_select .character_name_block,
body.charListGrid #rm_print_characters_block .character_select .character_name_block {
width: 100%;
}
body.charListGrid #rm_print_characters_block .bogus_folder_select .character_select_container,
body.charListGrid #rm_print_characters_block .character_select .character_select_container {
width: 100%;
justify-content: center;
@@ -68,6 +72,7 @@ body.charListGrid #rm_print_characters_block .group_select .group_name_block {
width: 100%;
}
body.charListGrid #rm_print_characters_block .bogus_folder_counter_block,
body.charListGrid #rm_print_characters_block .ch_description,
body.charListGrid #rm_print_characters_block .tags_inline,
body.charListGrid #rm_print_characters_block .character_version,
@@ -328,8 +333,7 @@ body.movingUI .drawer-content,
body.movingUI #expression-holder,
body.movingUI .zoomed_avatar,
body.movingUI .draggable,
body.movingUI #floatingPrompt,
body.movingUI #groupMemberListPopout {
body.movingUI #floatingPrompt {
resize: both;
}
@@ -353,4 +357,12 @@ body.expandMessageActions .mes .mes_buttons .extraMesButtons {
body.expandMessageActions .mes .mes_buttons .extraMesButtonsHint {
display: none !important;
}
}
#openai_image_inlining:not(:checked) ~ #image_inlining_hint {
display: none;
}
#openai_image_inlining:checked ~ #image_inlining_hint {
display: block;
}

View File

@@ -190,4 +190,4 @@
.WIEntryHeaderTitleMobile {
display: none;
}
}

View File

@@ -119,7 +119,7 @@
"Novel AI Model": "NovelAI 模型",
"No connection": "无连接",
"oobabooga/text-generation-webui": "",
"Make sure you run it with": "确保启动时包含 --api 参数",
"Make sure you run it with": "确保启动时包含 --extensions openai 参数",
"Blocking API url": "阻塞式 API 地址",
"Streaming API url": "流式传输 API 地址",
"to get your OpenAI API key.": "获取您的 OpenAI API 密钥。",
@@ -172,8 +172,6 @@
"Token Budget": "Token 预算",
"budget": "预算",
"Recursive scanning": "递归扫描",
"Soft Prompt": "软提示",
"About soft prompts": "关于软提示",
"None": "没有",
"User Settings": "聊天窗口设置",
"UI Customization": "聊天窗口定制",
@@ -469,7 +467,7 @@
"Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!": "通过发送一个短测试消息验证您的API连接。请注意您会获得相应的积分",
"Create New": "创建新的",
"Edit": "编辑",
"World Info & Soft Prompts": "世界背景 & 软提示",
"World Info": "世界背景",
"Locked = World Editor will stay open": "锁定=世界编辑器将保持打开状态",
"Entries can activate other entries by mentioning their keywords": "条目可以通过提及其关键字来激活其他条目",
"Lookup for the entry keys in the context will respect the case": "在上下文中查找条目键将遵守大小写",
@@ -672,7 +670,7 @@
"Novel AI Model": "NovelAI モデル",
"No connection": "接続なし",
"oobabooga/text-generation-webui": "",
"Make sure you run it with": "必ず --api の引数を含めて起動してください",
"Make sure you run it with": "必ず --extensions openai の引数を含めて起動してください",
"Blocking API url": "ブロッキング API URL",
"Streaming API url": "ストリーミング API URL",
"to get your OpenAI API key.": "あなたの OpenAI API キーを取得するために。",
@@ -724,8 +722,6 @@
"Token Budget": "トークン予算",
"budget": "予算",
"Recursive scanning": "再帰的スキャン",
"Soft Prompt": "ソフトプロンプト",
"About soft prompts": "ソフトプロンプトについて",
"None": "なし",
"User Settings": "ユーザー設定",
"UI Customization": "UIカスタマイズ",
@@ -1023,7 +1019,7 @@
"Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!": "NEEDS TRANSLATION",
"Create New": "NEEDS TRANSLATION",
"Edit": "NEEDS TRANSLATION",
"World Info & Soft Prompts": "NEEDS TRANSLATION",
"World Info": "NEEDS TRANSLATION",
"Locked = World Editor will stay open": "NEEDS TRANSLATION",
"Entries can activate other entries by mentioning their keywords": "NEEDS TRANSLATION",
"Lookup for the entry keys in the context will respect the case": "NEEDS TRANSLATION",
@@ -1227,7 +1223,7 @@
"Novel AI Model": "NovelAI 모델",
"No connection": "접속 실패",
"oobabooga/text-generation-webui": "oobabooga/text-generation-webui",
"Make sure you run it with": "--api 인수를 반드시 사용해야 합니다.",
"Make sure you run it with": "--extensions openai 인수를 반드시 사용해야 합니다.",
"Blocking API url": "API URL을 막는 중",
"Streaming API url": "API URL에서 스트리밍 중",
"OpenAI Model": "OpenAI 모델",
@@ -1278,8 +1274,6 @@
"Token Budget": "토큰 예산",
"budget": "예산",
"Recursive scanning": "되풀이 검색",
"Soft Prompt": "Soft Prompt",
"About soft prompts": "Soft prompt란?",
"None": "없음",
"User Settings": "사용자 설정",
"UI Customization": "UI 꾸미기",
@@ -1581,7 +1575,7 @@
"Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!": "짧은 시험 메시지를 보내서 API 접속 상태를 확인합니다. 서비스 사용으로 취급됩니다!",
"Create New": "새로 만들기",
"Edit": "수정하기",
"World Info & Soft Prompts": "세계관 & 소프트 프롬프트",
"World Info": "세계관",
"Locked = World Editor will stay open": "세계관 설정 패널 열림을 고정합니다",
"Entries can activate other entries by mentioning their keywords": "설정 내용에 다른 설정의 키워드가 있다면 연속으로 발동하게 합니다",
"Lookup for the entry keys in the context will respect the case": "설정 발동 키워드가 대소문자를 구분합니다",
@@ -1803,7 +1797,7 @@
"Novel AI Model": "Модель NovelAI",
"If you are using:": "Если вы используете:",
"oobabooga/text-generation-webui": "",
"Make sure you run it with": "Убедитесь, что при запуске указали аргумент --api",
"Make sure you run it with": "Убедитесь, что при запуске указали аргумент --extensions openai",
"Mancer AI": "",
"Use API key (Only required for Mancer)": "Нажмите на ячейку (и добавьте свой API ключ!):",
"Blocking API url": "Блокирующий API url",
@@ -1890,8 +1884,6 @@
"Token Budget": "Объем токенов",
"budget": "объем",
"Recursive scanning": "Рекурсивное сканирование",
"Soft Prompt": "Мягкая инструкция",
"About soft prompts": "О мягких инструкциях",
"None": "Отсутствует",
"User Settings": "Настройки пользователя",
"UI Mode": "Режим интерфейса",
@@ -2207,7 +2199,7 @@
"Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!": "Подверждает ваше соединение к API. Знайте, что за это снимут деньги с вашего счета.",
"Create New": "Создать новое",
"Edit": "Изменить",
"World Info & Soft Prompts": "Информация о Мире & Мягкий Промт",
"World Info": "Информация о Мире",
"Locked = World Editor will stay open": "Закреплено = Редактирование Мира останется открытым",
"Entries can activate other entries by mentioning their keywords": "Записи могут активировать другие записи если в них содержаться ключевые слова",
"Lookup for the entry keys in the context will respect the case": "Большая буква имеет значение при активации ключевого слова",
@@ -2413,7 +2405,7 @@
"Krake": "Krake",
"No connection": "Nessuna connessione",
"oobabooga/text-generation-webui": "oobabooga/text-generation-webui",
"Make sure you run it with": "assicurati di farlo partire con",
"Make sure you run it with": "assicurati di farlo partire con --extensions openai",
"Blocking API url": "Blocca l'indirizzo API",
"Streaming API url": "Streaming dell'indirizzo API",
"to get your OpenAI API key.": "per ottenere la tua chiave API di OpenAI.",
@@ -2469,8 +2461,6 @@
"Token Budget": "Budget per i Token",
"budget": "budget",
"Recursive scanning": "Analisi ricorsiva",
"Soft Prompt": "Prompt leggero",
"About soft prompts": "Riguardo i prompt leggeri",
"None": "None",
"User Settings": "Settaggi utente",
"UI Customization": "Personalizzazione UI",
@@ -2767,7 +2757,7 @@
"Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!": "Verifica la connessione all'API inviando un breve messaggio. Devi comprendere che il messaggio verrà addebitato come tutti gli altri!",
"Create New": "Crea nuovo",
"Edit": "Edita",
"World Info & Soft Prompts": "'Info Mondo' & Soft Prompt",
"World Info": "'Info Mondo'",
"Locked = World Editor will stay open": "Se clicchi il lucchetto, l'editor del mondo rimarrà aperto",
"Entries can activate other entries by mentioning their keywords": "Le voci possono attivare altre voci menzionando le loro parole chiave",
"Lookup for the entry keys in the context will respect the case": "Fai attenzione alle parole chiave usate, esse rispetteranno le maiuscole",
@@ -2963,7 +2953,7 @@
"Show tags in responses": "Mostra i tag nelle risposte",
"Story String": "Stringa narrativa",
"Text Adventure": "Avventura testuale",
"Text Gen WebUI (ooba/Mancer) presets": "Preset Text Gen WebUI (ooba/Mancer)",
"Text Gen WebUI presets": "Preset Text Gen WebUI",
"Toggle Panels": "Interruttore pannelli",
"Top A Sampling": "Top A Sampling",
"Top K Sampling": "Top K Sampling",
@@ -3174,7 +3164,7 @@
"Novel AI Model": "NovelAI-model",
"No connection": "Geen verbinding",
"oobabooga/text-generation-webui": "oobabooga/text-generation-webui",
"Make sure you run it with": "Zorg ervoor dat je het uitvoert met",
"Make sure you run it with": "Zorg ervoor dat je het uitvoert met --extensions openai",
"Blocking API url": "Blokkerende API-url",
"Streaming API url": "Streaming API-url",
"to get your OpenAI API key.": "om je OpenAI API-sleutel te verkrijgen.",
@@ -3226,8 +3216,6 @@
"Token Budget": "Token-budget",
"budget": "budget",
"Recursive scanning": "Recursieve scanning",
"Soft Prompt": "Zachte prompt",
"About soft prompts": "Over zachte prompts",
"None": "Geen",
"User Settings": "Gebruikersinstellingen",
"UI Customization": "UI-aanpassing",
@@ -3523,7 +3511,7 @@
"Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!": "Verifieert je API-verbinding door een kort testbericht te sturen. Wees je ervan bewust dat je hiervoor wordt gecrediteerd!",
"Create New": "Nieuw aanmaken",
"Edit": "Bewerken",
"World Info & Soft Prompts": "Wereldinformatie & Zachte Prompts",
"World Info": "Wereldinformatie",
"Locked = World Editor will stay open": "Vergrendeld = Wereld Editor blijft open",
"Entries can activate other entries by mentioning their keywords": "Invoeren kunnen andere invoeren activeren door hun trefwoorden te noemen",
"Lookup for the entry keys in the context will respect the case": "Zoeken naar de toetsen van de invoer in de context zal de hoofdlettergevoeligheid respecteren",
@@ -3727,7 +3715,7 @@
"Novel AI Model": "Modelo IA de NovelAI",
"No connection": "Desconectado",
"oobabooga/text-generation-webui": "oobabooga/text-generation-webui",
"Make sure you run it with": "Asegúrate de usar el argumento --api cuando se ejecute",
"Make sure you run it with": "Asegúrate de usar el argumento --extensions openai cuando se ejecute",
"Blocking API url": "API URL",
"Streaming API url": "Streaming API URL",
"to get your OpenAI API key.": "para conseguir tu clave API de OpenAI",

71
public/img/aphrodite.svg Normal file
View File

@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 500 500"
version="1.1"
id="svg6"
sodipodi:docname="aphrodite.svg"
inkscape:version="1.3 (0e150ed, 2023-07-21)"
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">
<defs
id="defs6" />
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.472"
inkscape:cx="251.05932"
inkscape:cy="250"
inkscape:window-width="1280"
inkscape:window-height="449"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="0"
inkscape:current-layer="svg6" />
<g
transform="matrix(1.3637143,0,0,1.2306337,286.98714,309.0439)"
id="b08450db-4034-4e8d-9232-9d086fc10fd0" />
<g
transform="matrix(1.3637143,0,0,1.2306337,286.98714,309.0439)"
id="54daa6c1-4b17-4e19-b0bb-42d1bcbfe659" />
<g
transform="matrix(1.3637143,0,0,1.2306337,186.0314,431.30731)"
id="g2" />
<g
transform="matrix(1.3637143,0,0,1.2306337,288.29633,320.27957)"
id="g3" />
<g
transform="matrix(1.686936,0,0,1.507445,388.05263,106.65182)"
id="g6"
style="">
<g
id="g5"
style="">
<path
style="opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;"
vector-effect="non-scaling-stroke"
d="m -189.927,161.041 32.809,-32.022 47.368,38.876 -32.619,43.738 -87.665,49.304 z"
stroke-linecap="round"
id="path3" />
<path
style="opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;"
vector-effect="non-scaling-stroke"
d="m -64.913,42.392 32.651,28.068 -77.49,97.438 -47.367,-38.878 91.346,-87.359 z"
stroke-linecap="round"
id="path4" />
<path
style="opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;"
vector-effect="non-scaling-stroke"
d="m 46.895,-67.722 -2.202,2.004 -110.467,107.379 33.512,28.799 95.769,-121.944 0.023,-0.025 c 2.011,-2.328 2.952,-5.03 2.819,-8.105 -0.131,-3.074 -1.3,-5.686 -3.502,-7.834 -2.205,-2.148 -4.846,-3.248 -7.922,-3.3 -3.077,-0.054 -5.754,0.955 -8.03,3.026 z"
stroke-linecap="round"
id="path5" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

3
public/img/mancer.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="128" height="128" viewBox="0 0 128 128" style="enable-background:new 0 0 128 128;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M115.36,61.84L70.22,50.49L114.45,2.4c0.41-0.45,0.43-1.13,0.05-1.6c-0.39-0.48-1.07-0.59-1.59-0.27 L12.3,61.98c-0.41,0.25-0.64,0.72-0.57,1.2c0.06,0.48,0.4,0.87,0.87,1.01l45.07,13.25L13.38,125.6c-0.42,0.46-0.44,1.15-0.04,1.61 c0.24,0.29,0.58,0.44,0.94,0.44c0.22,0,0.45-0.06,0.65-0.19l100.78-63.41c0.42-0.26,0.64-0.75,0.56-1.22 C116.19,62.34,115.84,61.95,115.36,61.84z" />
</svg>

After

Width:  |  Height:  |  Size: 561 B

View File

@@ -1,3 +1,3 @@
<svg width="33" height="41" viewBox="0 0 33 41" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.89418 31.9285C4.51814 29.6818 2.83212 27.8112 0.836131 26.521C0.26793 26.1537 0.124452 25.3382 0.540438 24.8047C4.15593 20.1672 9.79294 8.01868 12.7415 1.40215C13.181 0.416062 14.6883 0.738582 14.6883 1.81816V19.44C13.1242 20.1331 12.0332 21.6992 12.0332 23.5201C12.0332 24.1851 12.1787 24.8161 12.4397 25.383L5.89418 31.9285ZM7.34675 34.6814C8.03773 36.2042 8.61427 37.8368 9.07635 39.5334C9.19588 39.9722 9.59101 40.2824 10.0459 40.2824H16.4937H22.9416C23.3964 40.2824 23.7916 39.9722 23.9111 39.5334C24.3732 37.8368 24.9497 36.2042 25.6407 34.6814L22.211 31.2516L19.3551 34.1075C19.4281 34.3655 19.4672 34.6378 19.4672 34.9192C19.4672 36.5615 18.1358 37.8928 16.4935 37.8928C14.8512 37.8928 13.5198 36.5615 13.5198 34.9192C13.5198 33.2768 14.8512 31.9455 16.4935 31.9455C16.7448 31.9455 16.9888 31.9766 17.2219 32.0353L20.1083 29.1489L18.4762 27.5169C17.879 27.8137 17.2058 27.9806 16.4937 27.9806C15.7816 27.9806 15.1084 27.8137 14.5112 27.5169L7.34675 34.6814ZM27.0933 31.9285C28.4693 29.6818 30.1553 27.8112 32.1513 26.521C32.7195 26.1537 32.863 25.3382 32.447 24.8047C28.8315 20.1672 23.1945 8.01868 20.2459 1.40215C19.8065 0.416062 18.2992 0.738582 18.2992 1.81816V19.44C19.8632 20.1332 20.9542 21.6992 20.9542 23.5201C20.9542 24.1851 20.8087 24.8161 20.5478 25.383L27.0933 31.9285Z" fill="white"/>
<svg width="33" height="41" viewBox="0 0 33 41" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.89418 31.9285C4.51814 29.6818 2.83212 27.8112 0.836131 26.521C0.26793 26.1537 0.124452 25.3382 0.540438 24.8047C4.15593 20.1672 9.79294 8.01868 12.7415 1.40215C13.181 0.416062 14.6883 0.738582 14.6883 1.81816V19.44C13.1242 20.1331 12.0332 21.6992 12.0332 23.5201C12.0332 24.1851 12.1787 24.8161 12.4397 25.383L5.89418 31.9285ZM7.34675 34.6814C8.03773 36.2042 8.61427 37.8368 9.07635 39.5334C9.19588 39.9722 9.59101 40.2824 10.0459 40.2824H16.4937H22.9416C23.3964 40.2824 23.7916 39.9722 23.9111 39.5334C24.3732 37.8368 24.9497 36.2042 25.6407 34.6814L22.211 31.2516L19.3551 34.1075C19.4281 34.3655 19.4672 34.6378 19.4672 34.9192C19.4672 36.5615 18.1358 37.8928 16.4935 37.8928C14.8512 37.8928 13.5198 36.5615 13.5198 34.9192C13.5198 33.2768 14.8512 31.9455 16.4935 31.9455C16.7448 31.9455 16.9888 31.9766 17.2219 32.0353L20.1083 29.1489L18.4762 27.5169C17.879 27.8137 17.2058 27.9806 16.4937 27.9806C15.7816 27.9806 15.1084 27.8137 14.5112 27.5169L7.34675 34.6814ZM27.0933 31.9285C28.4693 29.6818 30.1553 27.8112 32.1513 26.521C32.7195 26.1537 32.863 25.3382 32.447 24.8047C28.8315 20.1672 23.1945 8.01868 20.2459 1.40215C19.8065 0.416062 18.2992 0.738582 18.2992 1.81816V19.44C19.8632 20.1332 20.9542 21.6992 20.9542 23.5201C20.9542 24.1851 20.8087 24.8161 20.5478 25.383L27.0933 31.9285Z" />
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,652 @@
"use strict";
import {
callPopup,
characters,
deleteCharacter,
event_types,
eventSource,
getCharacters,
getRequestHeaders,
printCharacters,
this_chid
} from "../script.js";
import { favsToHotswap } from "./RossAscends-mods.js";
import { hideLoader, showLoader } from "./loader.js";
import { convertCharacterToPersona } from "./personas.js";
import { createTagInput, getTagKeyForCharacter, tag_map } from "./tags.js";
// Utility object for popup messages.
const popupMessage = {
deleteChat(characterCount) {
return `<h3>Delete ${characterCount} characters?</h3>
<b>THIS IS PERMANENT!<br><br>
<label for="del_char_checkbox" class="checkbox_label justifyCenter">
<input type="checkbox" id="del_char_checkbox" />
<span>Also delete the chat files</span>
</label><br></b>`;
},
}
/**
* Static object representing the actions of the
* character context menu override.
*/
class CharacterContextMenu {
/**
* Tag one or more characters,
* opens a popup.
*
* @param selectedCharacters
*/
static tag = (selectedCharacters) => {
BulkTagPopupHandler.show(selectedCharacters);
}
/**
* Duplicate one or more characters
*
* @param characterId
* @returns {Promise<Response>}
*/
static duplicate = async (characterId) => {
const character = CharacterContextMenu.#getCharacter(characterId);
return fetch('/dupecharacter', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ avatar_url: character.avatar }),
});
}
/**
* Favorite a character
* and highlight it.
*
* @param characterId
* @returns {Promise<void>}
*/
static favorite = async (characterId) => {
const character = CharacterContextMenu.#getCharacter(characterId);
// Only set fav for V2 spec
const data = {
name: character.name,
avatar: character.avatar,
data: {
extensions: {
fav: !character.data.extensions.fav
}
}
};
return fetch('/v2/editcharacterattribute', {
method: "POST",
headers: getRequestHeaders(),
body: JSON.stringify(data),
}).then((response) => {
if (response.ok) {
const element = document.getElementById(`CharID${characterId}`);
element.classList.toggle('is_fav');
} else {
response.json().then(json => toastr.error('Character not saved. Error: ' + json.message + '. Field: ' + json.error));
}
});
}
/**
* Convert one or more characters to persona,
* may open a popup for one or more characters.
*
* @param characterId
* @returns {Promise<void>}
*/
static persona = async (characterId) => await convertCharacterToPersona(characterId);
/**
* Delete one or more characters,
* opens a popup.
*
* @param characterId
* @param deleteChats
* @returns {Promise<void>}
*/
static delete = async (characterId, deleteChats = false) => {
const character = CharacterContextMenu.#getCharacter(characterId);
return fetch('/deletecharacter', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ avatar_url: character.avatar, delete_chats: deleteChats }),
cache: 'no-cache',
}).then(response => {
if (response.ok) {
deleteCharacter(character.name, character.avatar).then(() => {
if (deleteChats) {
fetch("/getallchatsofcharacter", {
method: 'POST',
body: JSON.stringify({ avatar_url: character.avatar }),
headers: getRequestHeaders(),
}).then((response) => {
let data = response.json();
data = Object.values(data);
const pastChats = data.sort((a, b) => a["file_name"].localeCompare(b["file_name"])).reverse();
for (const chat of pastChats) {
const name = chat.file_name.replace('.jsonl', '');
eventSource.emit(event_types.CHAT_DELETED, name);
}
});
}
})
}
eventSource.emit('characterDeleted', { id: this_chid, character: characters[this_chid] });
});
}
static #getCharacter = (characterId) => characters[characterId] ?? null;
/**
* Show the context menu at the given position
*
* @param positionX
* @param positionY
*/
static show = (positionX, positionY) => {
let contextMenu = document.getElementById(BulkEditOverlay.contextMenuId);
contextMenu.style.left = `${positionX}px`;
contextMenu.style.top = `${positionY}px`;
document.getElementById(BulkEditOverlay.contextMenuId).classList.remove('hidden');
// Adjust position if context menu is outside of viewport
const boundingRect = contextMenu.getBoundingClientRect();
if (boundingRect.right > window.innerWidth) {
contextMenu.style.left = `${positionX - (boundingRect.right - window.innerWidth)}px`;
}
if (boundingRect.bottom > window.innerHeight) {
contextMenu.style.top = `${positionY - (boundingRect.bottom - window.innerHeight)}px`;
}
}
/**
* Hide the context menu
*/
static hide = () => document.getElementById(BulkEditOverlay.contextMenuId).classList.add('hidden');
/**
* Sets up the context menu for the given overlay
*
* @param characterGroupOverlay
*/
constructor(characterGroupOverlay) {
const contextMenuItems = [
{ id: 'character_context_menu_favorite', callback: characterGroupOverlay.handleContextMenuFavorite },
{ id: 'character_context_menu_duplicate', callback: characterGroupOverlay.handleContextMenuDuplicate },
{ id: 'character_context_menu_delete', callback: characterGroupOverlay.handleContextMenuDelete },
{ id: 'character_context_menu_persona', callback: characterGroupOverlay.handleContextMenuPersona },
{ id: 'character_context_menu_tag', callback: characterGroupOverlay.handleContextMenuTag }
];
contextMenuItems.forEach(contextMenuItem => document.getElementById(contextMenuItem.id).addEventListener('click', contextMenuItem.callback))
}
}
/**
* Represents a tag control not bound to a single character
*/
class BulkTagPopupHandler {
static #getHtml = (characterIds) => {
const characterData = JSON.stringify({ characterIds: characterIds });
return `<div id="bulk_tag_shadow_popup">
<div id="bulk_tag_popup">
<div id="bulk_tag_popup_holder">
<h3 class="m-b-1">Add tags to ${characterIds.length} characters</h3>
<br>
<div id="bulk_tags_div" class="marginBot5" data-characters='${characterData}'>
<div class="tag_controls">
<input id="bulkTagInput" class="text_pole tag_input wide100p margin0" data-i18n="[placeholder]Search / Create Tags" placeholder="Search / Create tags" maxlength="25" />
<div class="tags_view menu_button fa-solid fa-tags" title="View all tags" data-i18n="[title]View all tags"></div>
</div>
<div id="bulkTagList" class="m-t-1 tags"></div>
</div>
<div id="dialogue_popup_controls" class="m-t-1">
<div id="bulk_tag_popup_cancel" class="menu_button" data-i18n="Cancel">Close</div>
<div id="bulk_tag_popup_reset" class="menu_button" data-i18n="Cancel">Remove all</div>
</div>
</div>
</div>
</div>
`
};
/**
* Append and show the tag control
*
* @param characters - The characters assigned to this control
*/
static show(characters) {
document.body.insertAdjacentHTML('beforeend', this.#getHtml(characters));
createTagInput('#bulkTagInput', '#bulkTagList');
document.querySelector('#bulk_tag_popup_cancel').addEventListener('click', this.hide.bind(this));
document.querySelector('#bulk_tag_popup_reset').addEventListener('click', this.resetTags.bind(this, characters));
}
/**
* Hide and remove the tag control
*/
static hide() {
let popupElement = document.querySelector('#bulk_tag_shadow_popup');
if (popupElement) {
document.body.removeChild(popupElement);
}
printCharacters(true);
}
/**
* Empty the tag map for the given characters
*
* @param characterIds
*/
static resetTags(characterIds) {
characterIds.forEach((characterId) => {
const key = getTagKeyForCharacter(characterId);
if (key) tag_map[key] = [];
});
printCharacters(true);
}
}
class BulkEditOverlayState {
/**
*
* @type {number}
*/
static browse = 0;
/**
*
* @type {number}
*/
static select = 1;
}
/**
* Implement a SingletonPattern, allowing access to the group overlay instance
* from everywhere via (new CharacterGroupOverlay())
*
* @type BulkEditOverlay
*/
let bulkEditOverlayInstance = null;
class BulkEditOverlay {
static containerId = 'rm_print_characters_block';
static contextMenuId = 'character_context_menu';
static characterClass = 'character_select';
static groupClass = 'group_select';
static bogusFolderClass = 'bogus_folder_select';
static selectModeClass = 'group_overlay_mode_select';
static selectedClass = 'character_selected';
static legacySelectedClass = 'bulk_select_checkbox';
static longPressDelay = 2500;
#state = BulkEditOverlayState.browse;
#longPress = false;
#stateChangeCallbacks = [];
#selectedCharacters = [];
/**
* Locks other pointer actions when the context menu is open
*
* @type {boolean}
*/
#contextMenuOpen = false;
/**
* Whether the next character select should be skipped
*
* @type {boolean}
*/
#cancelNextToggle = false;
/**
* @type HTMLElement
*/
container = null;
get state() {
return this.#state;
}
set state(newState) {
if (this.#state === newState) return;
eventSource.emit(event_types.CHARACTER_GROUP_OVERLAY_STATE_CHANGE_BEFORE, newState)
.then(() => {
this.#state = newState;
eventSource.emit(event_types.CHARACTER_GROUP_OVERLAY_STATE_CHANGE_AFTER, this.state)
});
}
get isLongPress() {
return this.#longPress;
}
set isLongPress(longPress) {
this.#longPress = longPress;
}
get stateChangeCallbacks() {
return this.#stateChangeCallbacks;
}
/**
*
* @returns {*[]}
*/
get selectedCharacters() {
return this.#selectedCharacters;
}
constructor() {
if (bulkEditOverlayInstance instanceof BulkEditOverlay)
return bulkEditOverlayInstance
this.container = document.getElementById(BulkEditOverlay.containerId);
eventSource.on(event_types.CHARACTER_GROUP_OVERLAY_STATE_CHANGE_AFTER, this.handleStateChange);
bulkEditOverlayInstance = Object.freeze(this);
}
/**
* Set the overlay to browse mode
*/
browseState = () => this.state = BulkEditOverlayState.browse;
/**
* Set the overlay to select mode
*/
selectState = () => this.state = BulkEditOverlayState.select;
/**
* Set up a Sortable grid for the loaded page
*/
onPageLoad = () => {
this.browseState();
const elements = this.#getEnabledElements();
elements.forEach(element => element.addEventListener('touchstart', this.handleHold));
elements.forEach(element => element.addEventListener('mousedown', this.handleHold));
elements.forEach(element => element.addEventListener('contextmenu', this.handleDefaultContextMenu));
elements.forEach(element => element.addEventListener('touchend', this.handleLongPressEnd));
elements.forEach(element => element.addEventListener('mouseup', this.handleLongPressEnd));
elements.forEach(element => element.addEventListener('dragend', this.handleLongPressEnd));
elements.forEach(element => element.addEventListener('touchmove', this.handleLongPressEnd));
// Cohee: It only triggers when clicking on a margin between the elements?
// Feel free to fix or remove this, I'm not sure how to.
//this.container.addEventListener('click', this.handleCancelClick);
}
/**
* Handle state changes
*
*
*/
handleStateChange = () => {
switch (this.state) {
case BulkEditOverlayState.browse:
this.container.classList.remove(BulkEditOverlay.selectModeClass);
this.#contextMenuOpen = false;
this.#enableClickEventsForCharacters();
this.#enableClickEventsForGroups();
this.clearSelectedCharacters();
this.disableContextMenu();
this.#disableBulkEditButtonHighlight();
CharacterContextMenu.hide();
break;
case BulkEditOverlayState.select:
this.container.classList.add(BulkEditOverlay.selectModeClass);
this.#disableClickEventsForCharacters();
this.#disableClickEventsForGroups();
this.enableContextMenu();
this.#enableBulkEditButtonHighlight();
break;
}
this.stateChangeCallbacks.forEach(callback => callback(this.state));
}
/**
* Block the browsers native context menu and
* set a click event to hide the custom context menu.
*/
enableContextMenu = () => {
this.container.addEventListener('contextmenu', this.handleContextMenuShow);
document.addEventListener('click', this.handleContextMenuHide);
}
/**
* Remove event listeners, allowing the native browser context
* menu to be opened.
*/
disableContextMenu = () => {
this.container.removeEventListener('contextmenu', this.handleContextMenuShow);
document.removeEventListener('click', this.handleContextMenuHide);
}
handleDefaultContextMenu = (event) => {
if (this.isLongPress) {
event.preventDefault();
event.stopPropagation();
return false;
}
}
/**
* Opens menu on long-press.
*
* @param event - Pointer event
*/
handleHold = (event) => {
if (0 !== event.button && event.type !== 'touchstart') return;
if (this.#contextMenuOpen) {
this.#contextMenuOpen = false;
this.#cancelNextToggle = true;
CharacterContextMenu.hide();
return;
}
let cancel = false;
const cancelHold = (event) => cancel = true;
this.container.addEventListener('mouseup', cancelHold);
this.container.addEventListener('touchend', cancelHold);
this.isLongPress = true;
setTimeout(() => {
if (this.isLongPress && !cancel) {
if (this.state === BulkEditOverlayState.browse) {
this.selectState();
} else if (this.state === BulkEditOverlayState.select) {
this.#contextMenuOpen = true;
CharacterContextMenu.show(...this.#getContextMenuPosition(event));
}
}
this.container.removeEventListener('mouseup', cancelHold);
this.container.removeEventListener('touchend', cancelHold);
},
BulkEditOverlay.longPressDelay);
}
handleLongPressEnd = (event) => {
this.isLongPress = false;
if (this.#contextMenuOpen) event.stopPropagation();
}
handleCancelClick = () => {
if (false === this.#contextMenuOpen) this.state = BulkEditOverlayState.browse;
this.#contextMenuOpen = false;
}
/**
* Returns the position of the mouse/touch location
*
* @param event
* @returns {(boolean|number|*)[]}
*/
#getContextMenuPosition = (event) => [
event.clientX || event.touches[0].clientX,
event.clientY || event.touches[0].clientY,
];
#stopEventPropagation = (event) => {
if (this.#contextMenuOpen) {
this.handleContextMenuHide(event);
}
event.stopPropagation();
}
#enableClickEventsForGroups = () => this.#getDisabledElements().forEach((element) => element.removeEventListener('click', this.#stopEventPropagation));
#disableClickEventsForGroups = () => this.#getDisabledElements().forEach((element) => element.addEventListener('click', this.#stopEventPropagation));
#enableClickEventsForCharacters = () => this.#getEnabledElements().forEach(element => element.removeEventListener('click', this.toggleCharacterSelected));
#disableClickEventsForCharacters = () => this.#getEnabledElements().forEach(element => element.addEventListener('click', this.toggleCharacterSelected));
#enableBulkEditButtonHighlight = () => document.getElementById('bulkEditButton').classList.add('bulk_edit_overlay_active');
#disableBulkEditButtonHighlight = () => document.getElementById('bulkEditButton').classList.remove('bulk_edit_overlay_active');
#getEnabledElements = () => [...this.container.getElementsByClassName(BulkEditOverlay.characterClass)];
#getDisabledElements = () =>[...this.container.getElementsByClassName(BulkEditOverlay.groupClass), ...this.container.getElementsByClassName(BulkEditOverlay.bogusFolderClass)];
toggleCharacterSelected = event => {
event.stopPropagation();
const character = event.currentTarget;
const characterId = character.getAttribute('chid');
const alreadySelected = this.selectedCharacters.includes(characterId)
const legacyBulkEditCheckbox = character.querySelector('.' + BulkEditOverlay.legacySelectedClass);
// Only toggle when context menu is closed and wasn't just closed.
if (!this.#contextMenuOpen && !this.#cancelNextToggle)
if (alreadySelected) {
character.classList.remove(BulkEditOverlay.selectedClass);
if (legacyBulkEditCheckbox) legacyBulkEditCheckbox.checked = false;
this.dismissCharacter(characterId);
} else {
character.classList.add(BulkEditOverlay.selectedClass)
if (legacyBulkEditCheckbox) legacyBulkEditCheckbox.checked = true;
this.selectCharacter(characterId);
}
this.#cancelNextToggle = false;
}
handleContextMenuShow = (event) => {
event.preventDefault();
CharacterContextMenu.show(...this.#getContextMenuPosition(event));
this.#contextMenuOpen = true;
}
handleContextMenuHide = (event) => {
let contextMenu = document.getElementById(BulkEditOverlay.contextMenuId);
if (false === contextMenu.contains(event.target)) {
CharacterContextMenu.hide();
}
}
/**
* Concurrently handle character favorite requests.
*
* @returns {Promise<number>}
*/
handleContextMenuFavorite = () => Promise.all(this.selectedCharacters.map(async characterId => CharacterContextMenu.favorite(characterId)))
.then(() => getCharacters())
.then(() => favsToHotswap())
.then(() => this.browseState())
/**
* Concurrently handle character duplicate requests.
*
* @returns {Promise<number>}
*/
handleContextMenuDuplicate = () => Promise.all(this.selectedCharacters.map(async characterId => CharacterContextMenu.duplicate(characterId)))
.then(() => getCharacters())
.then(() => this.browseState())
/**
* Sequentially handle all character-to-persona conversions.
*
* @returns {Promise<void>}
*/
handleContextMenuPersona = async () => {
for (const characterId of this.selectedCharacters) {
await CharacterContextMenu.persona(characterId)
}
this.browseState();
}
/**
* Request user input before concurrently handle deletion
* requests.
*
* @returns {Promise<number>}
*/
handleContextMenuDelete = () => {
callPopup(
popupMessage.deleteChat(this.selectedCharacters.length), null)
.then((accept) => {
if (true !== accept) return;
const deleteChats = document.getElementById('del_char_checkbox').checked ?? false;
showLoader();
toastr.info("We're deleting your characters, please wait...", 'Working on it');
Promise.all(this.selectedCharacters.map(async characterId => CharacterContextMenu.delete(characterId, deleteChats)))
.then(() => getCharacters())
.then(() => this.browseState())
.finally(() => hideLoader());
}
);
}
/**
* Attaches and opens the tag menu
*/
handleContextMenuTag = () => {
CharacterContextMenu.tag(this.selectedCharacters);
}
addStateChangeCallback = callback => this.stateChangeCallbacks.push(callback);
selectCharacter = characterId => this.selectedCharacters.push(String(characterId));
dismissCharacter = characterId => this.#selectedCharacters = this.selectedCharacters.filter(item => String(characterId) !== item);
/**
* Clears internal character storage and
* removes visual highlight.
*/
clearSelectedCharacters = () => {
document.querySelectorAll('#' + BulkEditOverlay.containerId + ' .' + BulkEditOverlay.selectedClass)
.forEach(element => element.classList.remove(BulkEditOverlay.selectedClass));
this.selectedCharacters.length = 0;
}
}
export { BulkEditOverlayState, CharacterContextMenu, BulkEditOverlay };

View File

@@ -2,7 +2,7 @@
import { callPopup, event_types, eventSource, is_send_press, main_api, substituteParams } from "../script.js";
import { is_group_generating } from "./group-chats.js";
import { TokenHandler } from "./openai.js";
import { Message, TokenHandler } from "./openai.js";
import { power_user } from "./power-user.js";
import { debounce, waitUntilCondition, escapeHtml } from "./utils.js";
@@ -21,6 +21,16 @@ function debouncePromise(func, delay) {
};
}
const DEFAULT_DEPTH = 4;
/**
* @enum {number}
*/
export const INJECTION_POSITION = {
RELATIVE: 0,
ABSOLUTE: 1,
}
/**
* Register migrations for the prompt manager when settings are loaded or an Open AI preset is loaded.
*/
@@ -60,7 +70,7 @@ const registerPromptManagerMigration = () => {
* Represents a prompt.
*/
class Prompt {
identifier; role; content; name; system_prompt; position;
identifier; role; content; name; system_prompt; position; injection_position; injection_depth;
/**
* Create a new Prompt instance.
@@ -72,14 +82,18 @@ class Prompt {
* @param {string} param0.name - The name of the prompt.
* @param {boolean} param0.system_prompt - Indicates if the prompt is a system prompt.
* @param {string} param0.position - The position of the prompt in the prompt list.
* @param {number} param0.injection_position - The insert position of the prompt.
* @param {number} param0.injection_depth - The depth of the prompt in the chat.
*/
constructor({ identifier, role, content, name, system_prompt, position } = {}) {
constructor({ identifier, role, content, name, system_prompt, position, injection_depth, injection_position } = {}) {
this.identifier = identifier;
this.role = role;
this.content = content;
this.name = name;
this.system_prompt = system_prompt;
this.position = position;
this.injection_depth = injection_depth;
this.injection_position = injection_position;
}
}
@@ -275,7 +289,7 @@ PromptManagerModule.prototype.init = function (moduleConfiguration, serviceSetti
this.serviceSettings = serviceSettings;
this.containerElement = document.getElementById(this.configuration.containerIdentifier);
if ('global' === this.configuration.promptOrder.strategy) this.activeCharacter = {id: this.configuration.promptOrder.dummyId};
if ('global' === this.configuration.promptOrder.strategy) this.activeCharacter = { id: this.configuration.promptOrder.dummyId };
this.sanitizeServiceSettings();
@@ -381,6 +395,9 @@ PromptManagerModule.prototype.init = function (moduleConfiguration, serviceSetti
document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_name').value = prompt.name;
document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_role').value = 'system';
document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt').value = prompt.content;
document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position').value = prompt.injection_position ?? 0;
document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_depth').value = prompt.injection_depth ?? DEFAULT_DEPTH;
document.getElementById(this.configuration.prefix + 'prompt_manager_depth_block').style.visibility = prompt.injection_position === INJECTION_POSITION.ABSOLUTE ? 'visible' : 'hidden';
}
// Append prompt to selected character
@@ -673,6 +690,8 @@ PromptManagerModule.prototype.updatePromptWithPromptEditForm = function (prompt)
prompt.name = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_name').value;
prompt.role = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_role').value;
prompt.content = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt').value;
prompt.injection_position = Number(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position').value);
prompt.injection_depth = Number(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_depth').value);
}
/**
@@ -1085,10 +1104,16 @@ PromptManagerModule.prototype.loadPromptIntoEditForm = function (prompt) {
const nameField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_name');
const roleField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_role');
const promptField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt');
const injectionPositionField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position');
const injectionDepthField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_depth');
const injectionDepthBlock = document.getElementById(this.configuration.prefix + 'prompt_manager_depth_block');
nameField.value = prompt.name ?? '';
roleField.value = prompt.role ?? '';
promptField.value = prompt.content ?? '';
injectionPositionField.value = prompt.injection_position ?? INJECTION_POSITION.RELATIVE;
injectionDepthField.value = prompt.injection_depth ?? DEFAULT_DEPTH;
injectionDepthBlock.style.visibility = prompt.injection_position === INJECTION_POSITION.ABSOLUTE ? 'visible' : 'hidden';
const resetPromptButton = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_reset');
if (true === prompt.system_prompt) {
@@ -1098,10 +1123,23 @@ PromptManagerModule.prototype.loadPromptIntoEditForm = function (prompt) {
resetPromptButton.style.display = 'none';
}
injectionPositionField.removeEventListener('change', (e) => this.handleInjectionPositionChange(e));
injectionPositionField.addEventListener('change', (e) => this.handleInjectionPositionChange(e));
const savePromptButton = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_save');
savePromptButton.dataset.pmPrompt = prompt.identifier;
}
PromptManagerModule.prototype.handleInjectionPositionChange = function (event) {
const injectionDepthBlock = document.getElementById(this.configuration.prefix + 'prompt_manager_depth_block');
const injectionPosition = Number(event.target.value);
if (injectionPosition === INJECTION_POSITION.ABSOLUTE) {
injectionDepthBlock.style.visibility = 'visible';
} else {
injectionDepthBlock.style.visibility = 'hidden';
}
}
/**
* Loads a given prompt into the inspect form
* @param {MessageCollection} messages - Prompt object with properties 'name', 'role', 'content', and 'system_prompt'
@@ -1119,12 +1157,10 @@ PromptManagerModule.prototype.loadMessagesIntoInspectForm = function (messages)
let drawerHTML = `
<div class="inline-drawer ${this.configuration.prefix}prompt_manager_prompt">
<div class="inline-drawer-toggle inline-drawer-header">
<span>Name: ${title}, Role: ${role}, Tokens: ${tokens}</span>
<span>Name: ${escapeHtml(title)}, Role: ${role}, Tokens: ${tokens}</span>
<div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
</div>
<div class="inline-drawer-content">
${content}
</div>
<div class="inline-drawer-content" style="white-space: pre-wrap;">${escapeHtml(content)}</div>
</div>
`;
@@ -1135,9 +1171,11 @@ PromptManagerModule.prototype.loadMessagesIntoInspectForm = function (messages)
const messageList = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_inspect_list');
if (0 === messages.getCollection().length) messageList.innerHTML = `<span>This marker does not contain any prompts.</span>`;
const messagesCollection = messages instanceof Message ? [messages] : messages.getCollection();
messages.getCollection().forEach(message => {
if (0 === messagesCollection.length) messageList.innerHTML = `<span>This marker does not contain any prompts.</span>`;
messagesCollection.forEach(message => {
messageList.append(createInlineDrawer(message));
});
}
@@ -1152,10 +1190,16 @@ PromptManagerModule.prototype.clearEditForm = function () {
const nameField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_name');
const roleField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_role');
const promptField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt');
const injectionPositionField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position');
const injectionDepthField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_depth');
const injectionDepthBlock = document.getElementById(this.configuration.prefix + 'prompt_manager_depth_block');
nameField.value = '';
roleField.selectedIndex = 0;
promptField.value = '';
injectionPositionField.selectedIndex = 0;
injectionDepthField.value = DEFAULT_DEPTH;
injectionDepthBlock.style.visibility = 'unset';
roleField.disabled = false;
}
@@ -1435,13 +1479,18 @@ PromptManagerModule.prototype.renderPromptManagerListItems = function () {
}
const encodedName = escapeHtml(prompt.name);
const isSystemPrompt = !prompt.marker && prompt.system_prompt && prompt.injection_position !== INJECTION_POSITION.ABSOLUTE;
const isUserPrompt = !prompt.marker && !prompt.system_prompt && prompt.injection_position !== INJECTION_POSITION.ABSOLUTE;
const isInjectionPrompt = !prompt.marker && prompt.injection_position === INJECTION_POSITION.ABSOLUTE;
listItemHtml += `
<li class="${prefix}prompt_manager_prompt ${draggableClass} ${enabledClass} ${markerClass}" data-pm-identifier="${prompt.identifier}">
<span class="${prefix}prompt_manager_prompt_name" data-pm-name="${encodedName}">
${prompt.marker ? '<span class="fa-solid fa-thumb-tack" title="Marker"></span>' : ''}
${!prompt.marker && prompt.system_prompt ? '<span class="fa-solid fa-square-poll-horizontal" title="Global Prompt"></span>' : ''}
${!prompt.marker && !prompt.system_prompt ? '<span class="fa-solid fa-user" title="User Prompt"></span>' : ''}
${isSystemPrompt ? '<span class="fa-solid fa-square-poll-horizontal" title="Global Prompt"></span>' : ''}
${isUserPrompt ? '<span class="fa-solid fa-user" title="User Prompt"></span>' : ''}
${isInjectionPrompt ? `<span class="fa-solid fa-syringe" title="In-Chat Injection"></span>` : ''}
${this.isPromptInspectionAllowed(prompt) ? `<a class="prompt-manager-inspect-action">${encodedName}</a>` : encodedName}
${isInjectionPrompt ? `<small class="prompt-manager-injection-depth">@ ${prompt.injection_depth}</small>` : ''}
</span>
<span>
<span class="prompt_manager_prompt_controls">

View File

@@ -18,6 +18,8 @@ import {
getThumbnailUrl,
selectCharacterById,
eventSource,
menu_type,
substituteParams,
} from "../script.js";
import {
@@ -31,9 +33,10 @@ import {
SECRET_KEYS,
secret_state,
} from "./secrets.js";
import { debounce, delay, getStringHash, isUrlOrAPIKey, waitUntilCondition } from "./utils.js";
import { debounce, delay, getStringHash, isValidUrl, waitUntilCondition } from "./utils.js";
import { chat_completion_sources, oai_settings } from "./openai.js";
import { getTokenCount } from "./tokenizers.js";
import { isMancer } from "./textgen-settings.js";
var RPanelPin = document.getElementById("rm_button_panel_pin");
@@ -57,9 +60,7 @@ const countTokensDebounced = debounce(RA_CountCharTokens, 1000);
const observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
if (mutation.target.id === "online_status_text2" ||
mutation.target.id === "online_status_text3" ||
mutation.target.classList.contains("online_status_text4")) {
if (mutation.target.classList.contains("online_status_text")) {
RA_checkOnlineStatus();
} else if (mutation.target.parentNode === SelectedCharacterTab) {
setTimeout(RA_CountCharTokens, 200);
@@ -171,7 +172,7 @@ export function humanizedDateTime() {
let humanMillisecond =
(baseDate.getMilliseconds() < 10 ? "0" : "") + baseDate.getMilliseconds();
let HumanizedDateTime =
humanYear + "-" + humanMonth + "-" + humanDate + " @" + humanHour + "h " + humanMinute + "m " + humanSecond + "s " + humanMillisecond + "ms";
humanYear + "-" + humanMonth + "-" + humanDate + "@" + humanHour + "h" + humanMinute + "m" + humanSecond + "s";
return HumanizedDateTime;
}
@@ -234,7 +235,9 @@ export function RA_CountCharTokens() {
total_tokens += Number(counter.text());
permanent_tokens += isPermanent ? Number(counter.text()) : 0;
} else {
const tokens = getTokenCount(value);
// We substitute macro for existing characters, but not for the character being created
const valueToCount = menu_type === 'create' ? value : substituteParams(value);
const tokens = getTokenCount(valueToCount);
counter.text(tokens);
total_tokens += tokens;
permanent_tokens += isPermanent ? tokens : 0;
@@ -264,11 +267,11 @@ async function RA_autoloadchat() {
let active_character_id = Object.keys(characters).find(key => characters[key].avatar === active_character);
if (active_character_id !== null) {
selectCharacterById(String(active_character_id));
await selectCharacterById(String(active_character_id));
}
if (active_group != null) {
openGroupById(String(active_group));
await openGroupById(String(active_group));
}
// if the character list hadn't been loaded yet, try again.
@@ -368,7 +371,7 @@ function RA_checkOnlineStatus() {
connection_made = false;
} else {
if (online_status !== undefined && online_status !== "no_connection") {
$("#send_textarea").attr("placeholder", `Type a message, or /? for command list`); //on connect, placeholder tells user to type message
$("#send_textarea").attr("placeholder", `Type a message, or /? for help`); //on connect, placeholder tells user to type message
$('#send_form').removeClass("no-connection");
$("#API-status-top").removeClass("fa-plug-circle-exclamation redOverlayGlow");
$("#API-status-top").addClass("fa-plug");
@@ -394,18 +397,21 @@ function RA_autoconnect(PrevApi) {
if (online_status === "no_connection" && LoadLocalBool('AutoConnectEnabled')) {
switch (main_api) {
case 'kobold':
if (api_server && isUrlOrAPIKey(api_server)) {
$("#api_button").click();
if (api_server && isValidUrl(api_server)) {
$("#api_button").trigger('click');
}
break;
case 'novel':
if (secret_state[SECRET_KEYS.NOVEL]) {
$("#api_button_novel").click();
$("#api_button_novel").trigger('click');
}
break;
case 'textgenerationwebui':
if (api_server_textgenerationwebui && isUrlOrAPIKey(api_server_textgenerationwebui)) {
$("#api_button_textgenerationwebui").click();
if (isMancer() && secret_state[SECRET_KEYS.MANCER]) {
$("#api_button_textgenerationwebui").trigger('click');
}
else if (api_server_textgenerationwebui && isValidUrl(api_server_textgenerationwebui)) {
$("#api_button_textgenerationwebui").trigger('click');
}
break;
case 'openai':
@@ -417,7 +423,7 @@ function RA_autoconnect(PrevApi) {
|| (secret_state[SECRET_KEYS.AI21] && oai_settings.chat_completion_source == chat_completion_sources.AI21)
|| (secret_state[SECRET_KEYS.PALM] && oai_settings.chat_completion_source == chat_completion_sources.PALM)
) {
$("#api_button_openai").click();
$("#api_button_openai").trigger('click');
}
break;
}
@@ -425,8 +431,8 @@ function RA_autoconnect(PrevApi) {
if (!connection_made) {
RA_AC_retries++;
retry_delay = Math.min(retry_delay * 2, 30000); // double retry delay up to to 30 secs
//console.log('connection attempts: ' + RA_AC_retries + ' delay: ' + (retry_delay / 1000) + 's');
setTimeout(RA_autoconnect, retry_delay);
// console.log('connection attempts: ' + RA_AC_retries + ' delay: ' + (retry_delay / 1000) + 's');
// setTimeout(RA_autoconnect, retry_delay);
}
}
}
@@ -890,13 +896,19 @@ export function initRossMods() {
const originalScrollBottom = chatBlock[0].scrollHeight - (chatBlock.scrollTop() + chatBlock.outerHeight());
this.style.height = window.getComputedStyle(this).getPropertyValue('min-height');
this.style.height = (this.scrollHeight) + 'px';
const newScrollTop = chatBlock[0].scrollHeight - (chatBlock.outerHeight() + originalScrollBottom);
const newScrollTop = Math.round(chatBlock[0].scrollHeight - (chatBlock.outerHeight() + originalScrollBottom));
chatBlock.scrollTop(newScrollTop);
});
//Regenerate if user swipes on the last mesage in chat
document.addEventListener('swiped-left', function (e) {
if (power_user.gestures === false) {
return
}
if ($(".mes_edit_buttons, #character_popup, #dialogue_popup, #WorldInfo").is(":visible")) {
return
}
var SwipeButR = $('.swipe_right:last');
var SwipeTargetMesClassParent = $(e.target).closest('.last_mes');
if (SwipeTargetMesClassParent !== null) {
@@ -906,6 +918,12 @@ export function initRossMods() {
}
});
document.addEventListener('swiped-right', function (e) {
if (power_user.gestures === false) {
return
}
if ($(".mes_edit_buttons, #character_popup, #dialogue_popup, #WorldInfo").is(":visible")) {
return
}
var SwipeButL = $('.swipe_left:last');
var SwipeTargetMesClassParent = $(e.target).closest('.last_mes');
if (SwipeTargetMesClassParent !== null) {

View File

@@ -235,6 +235,7 @@ function loadSettings() {
chat_metadata[metadata_keys.depth] = chat_metadata[metadata_keys.depth] ?? extension_settings.note.defaultDepth ?? DEFAULT_DEPTH;
$('#extension_floating_prompt').val(chat_metadata[metadata_keys.prompt]);
$('#extension_floating_interval').val(chat_metadata[metadata_keys.interval]);
$('#extension_floating_allow_wi_scan').prop('checked', extension_settings.note.allowWIScan ?? false);
$('#extension_floating_depth').val(chat_metadata[metadata_keys.depth]);
$(`input[name="extension_floating_position"][value="${chat_metadata[metadata_keys.position]}"]`).prop('checked', true);
@@ -389,6 +390,11 @@ function onChatChanged() {
$('#extension_floating_default_token_counter').text(tokenCounter3);
}
function onAllowWIScanCheckboxChanged() {
extension_settings.note.allowWIScan = !!$(this).prop('checked');
updateSettings();
}
/**
* Inject author's note options and setup event listeners.
*/
@@ -402,6 +408,7 @@ export function initAuthorsNote() {
$('#extension_floating_default').on('input', onExtensionFloatingDefaultInput);
$('#extension_default_depth').on('input', onDefaultDepthInput);
$('#extension_default_interval').on('input', onDefaultIntervalInput);
$('#extension_floating_allow_wi_scan').on('input', onAllowWIScanCheckboxChanged);
$('input[name="extension_floating_position"]').on('change', onExtensionFloatingPositionInput);
$('input[name="extension_default_position"]').on('change', onDefaultPositionInput);
$('input[name="extension_floating_char_position"]').on('change', onExtensionFloatingCharPositionInput);

View File

@@ -0,0 +1,488 @@
import { callPopup, chat_metadata, eventSource, event_types, generateQuietPrompt, getCurrentChatId, getRequestHeaders, getThumbnailUrl } from "../script.js";
import { saveMetadataDebounced } from "./extensions.js";
import { registerSlashCommand } from "./slash-commands.js";
import { stringFormat } from "./utils.js";
const BG_METADATA_KEY = 'custom_background';
const LIST_METADATA_KEY = 'chat_backgrounds';
/**
* Sets the background for the current chat and adds it to the list of custom backgrounds.
* @param {{url: string, path:string}} backgroundInfo
*/
function forceSetBackground(backgroundInfo) {
saveBackgroundMetadata(backgroundInfo.url);
setCustomBackground();
const list = chat_metadata[LIST_METADATA_KEY] || [];
const bg = backgroundInfo.path;
list.push(bg);
chat_metadata[LIST_METADATA_KEY] = list;
saveMetadataDebounced();
getChatBackgroundsList();
highlightNewBackground(bg);
highlightLockedBackground();
}
async function onChatChanged() {
if (hasCustomBackground()) {
setCustomBackground();
}
else {
unsetCustomBackground();
}
getChatBackgroundsList();
highlightLockedBackground();
}
function getChatBackgroundsList() {
const list = chat_metadata[LIST_METADATA_KEY];
const listEmpty = !Array.isArray(list) || list.length === 0;
$('#bg_custom_content').empty();
$('#bg_chat_hint').toggle(listEmpty);
if (listEmpty) {
return;
}
for (const bg of list) {
const template = getBackgroundFromTemplate(bg, true);
$('#bg_custom_content').append(template);
}
}
function getBackgroundPath(fileUrl) {
return `backgrounds/${fileUrl}`;
}
function highlightLockedBackground() {
$('.bg_example').removeClass('locked');
const lockedBackground = chat_metadata[BG_METADATA_KEY];
if (!lockedBackground) {
return;
}
$(`.bg_example`).each(function () {
const url = $(this).data('url');
if (url === lockedBackground) {
$(this).addClass('locked');
}
});
}
function onLockBackgroundClick(e) {
e.stopPropagation();
const chatName = getCurrentChatId();
if (!chatName) {
toastr.warning('Select a chat to lock the background for it');
return;
}
const relativeBgImage = getUrlParameter(this);
saveBackgroundMetadata(relativeBgImage);
setCustomBackground();
highlightLockedBackground();
}
function onUnlockBackgroundClick(e) {
e.stopPropagation();
removeBackgroundMetadata();
unsetCustomBackground();
highlightLockedBackground();
}
function hasCustomBackground() {
return chat_metadata[BG_METADATA_KEY];
}
function saveBackgroundMetadata(file) {
chat_metadata[BG_METADATA_KEY] = file;
saveMetadataDebounced();
}
function removeBackgroundMetadata() {
delete chat_metadata[BG_METADATA_KEY];
saveMetadataDebounced();
}
function setCustomBackground() {
const file = chat_metadata[BG_METADATA_KEY];
// bg already set
if (document.getElementById("bg_custom").style.backgroundImage == file) {
return;
}
$("#bg_custom").css("background-image", file);
}
function unsetCustomBackground() {
$("#bg_custom").css("background-image", 'none');
}
function onSelectBackgroundClick() {
const isCustom = $(this).attr('custom') === 'true';
const relativeBgImage = getUrlParameter(this);
// if clicked on upload button
if (!relativeBgImage) {
return;
}
// Automatically lock the background if it's custom or other background is locked
if (hasCustomBackground() || isCustom) {
saveBackgroundMetadata(relativeBgImage);
setCustomBackground();
highlightLockedBackground();
} else {
highlightLockedBackground();
}
const customBg = window.getComputedStyle(document.getElementById('bg_custom')).backgroundImage;
// Custom background is set. Do not override the layer below
if (customBg !== 'none') {
return;
}
const bgFile = $(this).attr("bgfile");
const backgroundUrl = getBackgroundPath(bgFile);
// Fetching to browser memory to reduce flicker
fetch(backgroundUrl).then(() => {
$("#bg1").css("background-image", relativeBgImage);
setBackground(bgFile);
}).catch(() => {
console.log('Background could not be set: ' + backgroundUrl);
});
}
async function onCopyToSystemBackgroundClick(e) {
e.stopPropagation();
const bgNames = await getNewBackgroundName(this);
if (!bgNames) {
return;
}
const bgFile = await fetch(bgNames.oldBg);
if (!bgFile.ok) {
toastr.warning('Failed to copy background');
return;
}
const blob = await bgFile.blob();
const file = new File([blob], bgNames.newBg);
const formData = new FormData();
formData.set('avatar', file);
uploadBackground(formData);
const list = chat_metadata[LIST_METADATA_KEY] || [];
const index = list.indexOf(bgNames.oldBg);
list.splice(index, 1);
saveMetadataDebounced();
getChatBackgroundsList();
}
/**
* Gets the new background name from the user.
* @param {Element} referenceElement
* @returns {Promise<{oldBg: string, newBg: string}>}
* */
async function getNewBackgroundName(referenceElement) {
const exampleBlock = $(referenceElement).closest('.bg_example');
const isCustom = exampleBlock.attr('custom') === 'true';
const oldBg = exampleBlock.attr('bgfile');
if (!oldBg) {
console.debug('no bgfile');
return;
}
const fileExtension = oldBg.split('.').pop();
const fileNameBase = isCustom ? oldBg.split('/').pop() : oldBg;
const oldBgExtensionless = fileNameBase.replace(`.${fileExtension}`, '');
const newBgExtensionless = await callPopup('<h3>Enter new background name:</h3>', 'input', oldBgExtensionless);
if (!newBgExtensionless) {
console.debug('no new_bg_extensionless');
return;
}
const newBg = `${newBgExtensionless}.${fileExtension}`;
if (oldBgExtensionless === newBgExtensionless) {
console.debug('new_bg === old_bg');
return;
}
return { oldBg, newBg };
}
async function onRenameBackgroundClick(e) {
e.stopPropagation();
const bgNames = await getNewBackgroundName(this);
if (!bgNames) {
return;
}
const data = { old_bg: bgNames.oldBg, new_bg: bgNames.newBg };
const response = await fetch('/renamebackground', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(data),
cache: 'no-cache',
});
if (response.ok) {
await getBackgrounds();
highlightNewBackground(bgNames.newBg);
} else {
toastr.warning('Failed to rename background');
}
}
async function onDeleteBackgroundClick(e) {
e.stopPropagation();
const bgToDelete = $(this).closest('.bg_example');
const url = bgToDelete.data('url');
const isCustom = bgToDelete.attr('custom') === 'true';
const confirm = await callPopup("<h3>Delete the background?</h3>", 'confirm');
const bg = bgToDelete.attr('bgfile');
if (confirm) {
// If it's not custom, it's a built-in background. Delete it from the server
if (!isCustom) {
delBackground(bg);
} else {
const list = chat_metadata[LIST_METADATA_KEY] || [];
const index = list.indexOf(bg);
list.splice(index, 1);
}
const siblingSelector = '.bg_example:not(#form_bg_download)';
const nextBg = bgToDelete.next(siblingSelector);
const prevBg = bgToDelete.prev(siblingSelector);
const anyBg = $(siblingSelector);
if (nextBg.length > 0) {
nextBg.trigger('click');
} else if (prevBg.length > 0) {
prevBg.trigger('click');
} else {
$(anyBg[Math.floor(Math.random() * anyBg.length)]).trigger('click');
}
bgToDelete.remove();
if (url === chat_metadata[BG_METADATA_KEY]) {
removeBackgroundMetadata();
unsetCustomBackground();
highlightLockedBackground();
}
if (isCustom) {
getChatBackgroundsList();
saveMetadataDebounced();
}
}
}
const autoBgPrompt = `Pause your roleplay and choose a location ONLY from the provided list that is the most suitable for the current scene. Do not output any other text:\n{0}`;
async function autoBackgroundCommand() {
/** @type {HTMLElement[]} */
const bgTitles = Array.from(document.querySelectorAll('#bg_menu_content .BGSampleTitle'));
const options = bgTitles.map(x => ({ element: x, text: x.innerText.trim() })).filter(x => x.text.length > 0);
if (options.length == 0) {
toastr.warning('No backgrounds to choose from. Please upload some images to the "backgrounds" folder.');
return;
}
const list = options.map(option => `- ${option.text}`).join('\n');
const prompt = stringFormat(autoBgPrompt, list);
const reply = await generateQuietPrompt(prompt, false, false);
const fuse = new Fuse(options, { keys: ['text'] });
const bestMatch = fuse.search(reply, { limit: 1 });
if (bestMatch.length == 0) {
toastr.warning('No match found. Please try again.');
return;
}
console.debug('Automatically choosing background:', bestMatch);
bestMatch[0].item.element.click();
}
export async function getBackgrounds() {
const response = await fetch("/getbackgrounds", {
method: "POST",
headers: getRequestHeaders(),
body: JSON.stringify({
"": "",
}),
});
if (response.ok === true) {
const getData = await response.json();
//background = getData;
//console.log(getData.length);
$("#bg_menu_content").children('div').remove();
for (const bg of getData) {
const template = getBackgroundFromTemplate(bg, false);
$("#bg_menu_content").append(template);
}
}
}
/**
* Gets the URL of the background
* @param {Element} block
* @returns {string} URL of the background
*/
function getUrlParameter(block) {
return $(block).closest(".bg_example").data("url");
}
/**
* Instantiates a background template
* @param {string} bg Path to background
* @param {boolean} isCustom Whether the background is custom
* @returns {JQuery<HTMLElement>} Background template
*/
function getBackgroundFromTemplate(bg, isCustom) {
const template = $('#background_template .bg_example').clone();
const thumbPath = isCustom ? bg : getThumbnailUrl('bg', bg);
const url = isCustom ? `url("${encodeURI(bg)}")` : `url("${getBackgroundPath(bg)}")`;
const title = isCustom ? bg.split('/').pop() : bg;
const friendlyTitle = title.slice(0, title.lastIndexOf('.'));
template.attr('title', title);
template.attr('bgfile', bg);
template.attr('custom', String(isCustom));
template.data('url', url);
template.css('background-image', `url('${thumbPath}')`);
template.find('.BGSampleTitle').text(friendlyTitle);
return template;
}
async function setBackground(bg) {
jQuery.ajax({
type: "POST", //
url: "/setbackground", //
data: JSON.stringify({
bg: bg,
}),
beforeSend: function () {
},
cache: false,
dataType: "json",
contentType: "application/json",
//processData: false,
success: function (html) { },
error: function (jqXHR, exception) {
console.log(exception);
console.log(jqXHR);
},
});
}
async function delBackground(bg) {
const response = await fetch("/delbackground", {
method: "POST",
headers: getRequestHeaders(),
body: JSON.stringify({
bg: bg,
}),
});
}
function onBackgroundUploadSelected() {
const form = $("#form_bg_download").get(0);
if (!(form instanceof HTMLFormElement)) {
console.error('form_bg_download is not a form');
return;
}
const formData = new FormData(form);
uploadBackground(formData);
form.reset();
}
/**
* Uploads a background to the server
* @param {FormData} formData
*/
function uploadBackground(formData) {
jQuery.ajax({
type: "POST",
url: "/downloadbackground",
data: formData,
beforeSend: function () {
},
cache: false,
contentType: false,
processData: false,
success: async function (bg) {
setBackground(bg);
$("#bg1").css("background-image", `url("${getBackgroundPath(bg)}"`);
await getBackgrounds();
highlightNewBackground(bg);
},
error: function (jqXHR, exception) {
console.log(exception);
console.log(jqXHR);
},
});
}
/**
* @param {string} bg
*/
function highlightNewBackground(bg) {
const newBg = $(`.bg_example[bgfile="${bg}"]`);
const scrollOffset = newBg.offset().top - newBg.parent().offset().top;
$('#Backgrounds').scrollTop(scrollOffset);
newBg.addClass('flash animated');
setTimeout(() => newBg.removeClass('flash animated'), 2000);
}
function onBackgroundFilterInput() {
const filterValue = String($(this).val()).toLowerCase();
$("#bg_menu_content > div").each(function () {
const $bgContent = $(this);
if ($bgContent.attr("title").toLowerCase().includes(filterValue)) {
$bgContent.show();
} else {
$bgContent.hide();
}
});
}
export function initBackgrounds() {
eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
eventSource.on(event_types.FORCE_SET_BACKGROUND, forceSetBackground);
$(document).on("click", '.bg_example', onSelectBackgroundClick);
$(document).on('click', '.bg_example_lock', onLockBackgroundClick);
$(document).on('click', '.bg_example_unlock', onUnlockBackgroundClick);
$(document).on('click', '.bg_example_edit', onRenameBackgroundClick);
$(document).on("click", '.bg_example_cross', onDeleteBackgroundClick);
$(document).on("click", '.bg_example_copy', onCopyToSystemBackgroundClick);
$('#auto_background').on("click", autoBackgroundCommand);
$("#add_bg_button").on('change', onBackgroundUploadSelected);
$("#bg-filter").on("input", onBackgroundFilterInput);
registerSlashCommand('lockbg', onLockBackgroundClick, ['bglock'], " locks a background for the currently selected chat", true, true);
registerSlashCommand('unlockbg', onUnlockBackgroundClick, ['bgunlock'], ' unlocks a background for the currently selected chat', true, true);
registerSlashCommand('autobg', autoBackgroundCommand, ['bgauto'], ' automatically changes the background based on the chat context using the AI request prompt', true, true);
}

View File

@@ -1,24 +1,44 @@
import { characters, getCharacters, handleDeleteCharacter, callPopup } from "../../../script.js";
import { characters, getCharacters, handleDeleteCharacter, callPopup } from "../script.js";
import {BulkEditOverlay, BulkEditOverlayState} from "./BulkEditOverlay.js";
let is_bulk_edit = false;
const enableBulkEdit = () => {
enableBulkSelect();
(new BulkEditOverlay()).selectState();
// show the delete button
$("#bulkDeleteButton").show();
is_bulk_edit = true;
}
const disableBulkEdit = () => {
disableBulkSelect();
(new BulkEditOverlay()).browseState();
// hide the delete button
$("#bulkDeleteButton").hide();
is_bulk_edit = false;
}
const toggleBulkEditMode = (isBulkEdit) => {
if (isBulkEdit) {
disableBulkEdit();
} else {
enableBulkEdit();
}
}
(new BulkEditOverlay()).addStateChangeCallback((state) => {
if (state === BulkEditOverlayState.select) enableBulkEdit();
if (state === BulkEditOverlayState.browse) disableBulkEdit();
});
/**
* Toggles bulk edit mode on/off when the edit button is clicked.
*/
function onEditButtonClick() {
console.log("Edit button clicked");
// toggle bulk edit mode
if (is_bulk_edit) {
disableBulkSelect();
// hide the delete button
$("#bulkDeleteButton").hide();
is_bulk_edit = false;
} else {
enableBulkSelect();
// show the delete button
$("#bulkDeleteButton").show();
is_bulk_edit = true;
}
toggleBulkEditMode(is_bulk_edit);
}
/**
@@ -64,23 +84,6 @@ async function onDeleteButtonClick() {
}
}
/**
* Adds the bulk edit and delete buttons to the UI.
*/
function addButtons() {
const editButton = $(
"<i id='bulkEditButton' class='fa-solid fa-edit menu_button bulkEditButton' title='Bulk edit characters'></i>"
);
const deleteButton = $(
"<i id='bulkDeleteButton' class='fa-solid fa-trash menu_button bulkDeleteButton' title='Bulk delete characters' style='display: none;'></i>"
);
$("#charListGridToggle").after(editButton, deleteButton);
$("#bulkEditButton").on("click", onEditButtonClick);
$("#bulkDeleteButton").on("click", onDeleteButtonClick);
}
/**
* Enables bulk selection by adding a checkbox next to each character.
*/
@@ -111,7 +114,7 @@ function disableBulkSelect() {
/**
* Entry point that runs on page load.
*/
jQuery(async () => {
addButtons();
// loadSettings();
jQuery(() => {
$("#bulkEditButton").on("click", onEditButtonClick);
$("#bulkDeleteButton").on("click", onDeleteButtonClick);
});

View File

@@ -1,19 +1,17 @@
import {
chat_metadata,
substituteParams,
this_chid,
eventSource,
event_types,
saveSettingsDebounced,
this_chid,
} from "../../../script.js";
import { selected_group } from "../../group-chats.js";
import { extension_settings, saveMetadataDebounced } from "../../extensions.js";
import { getCharaFilename, delay } from "../../utils.js";
import { power_user } from "../../power-user.js";
import { metadataKeys } from "./util.js";
} from "../script.js";
import { extension_settings, saveMetadataDebounced } from "./extensions.js"
import { selected_group } from "./group-chats.js";
import { getCharaFilename, delay } from "./utils.js";
import { power_user } from "./power-user.js";
// Keep track of where your extension is located, name should match repo name
const extensionName = "cfg";
const extensionFolderPath = `scripts/extensions/${extensionName}`;
const extensionName = 'cfg';
const defaultSettings = {
global: {
"guidance_scale": 1,
@@ -180,7 +178,7 @@ async function modifyCharaHtml() {
function loadSettings() {
// Set chat CFG if it exists
$('#chat_cfg_guidance_scale').val(chat_metadata[metadataKeys.guidance_scale] ?? 1.0.toFixed(2));
$('#chat_cfg_guidance_scale_counter').text(chat_metadata[metadataKeys.guidance_scale]?.toFixed(2) ?? 1.0.toFixed(2));
$('#chat_cfg_guidance_scale_counter').val(chat_metadata[metadataKeys.guidance_scale]?.toFixed(2) ?? 1.0.toFixed(2));
$('#chat_cfg_negative_prompt').val(chat_metadata[metadataKeys.negative_prompt] ?? '');
$('#chat_cfg_positive_prompt').val(chat_metadata[metadataKeys.positive_prompt] ?? '');
$('#groupchat_cfg_use_chara').prop('checked', chat_metadata[metadataKeys.groupchat_individual_chars] ?? false);
@@ -199,7 +197,7 @@ function loadSettings() {
if (!promptSeparator.startsWith(`"`)) {
promptSeparatorDisplay.unshift(`"`);
}
if (!promptSeparator.endsWith(`"`)) {
promptSeparatorDisplay.push(`"`);
}
@@ -213,7 +211,7 @@ function loadSettings() {
if (!selected_group) {
const charaCfg = extension_settings.cfg.chara.find((e) => e.name === getCharaFilename());
$('#chara_cfg_guidance_scale').val(charaCfg?.guidance_scale ?? 1.00);
$('#chara_cfg_guidance_scale_counter').text(charaCfg?.guidance_scale?.toFixed(2) ?? 1.0.toFixed(2));
$('#chara_cfg_guidance_scale_counter').val(charaCfg?.guidance_scale?.toFixed(2) ?? 1.0.toFixed(2));
$('#chara_cfg_negative_prompt').val(charaCfg?.negative_prompt ?? '');
$('#chara_cfg_positive_prompt').val(charaCfg?.positive_prompt ?? '');
}
@@ -230,7 +228,7 @@ async function initialLoadSettings() {
// Set global CFG values on load
$('#global_cfg_guidance_scale').val(extension_settings.cfg.global.guidance_scale);
$('#global_cfg_guidance_scale_counter').text(extension_settings.cfg.global.guidance_scale.toFixed(2));
$('#global_cfg_guidance_scale_counter').val(extension_settings.cfg.global.guidance_scale.toFixed(2));
$('#global_cfg_negative_prompt').val(extension_settings.cfg.global.negative_prompt);
$('#global_cfg_positive_prompt').val(extension_settings.cfg.global.positive_prompt);
}
@@ -279,14 +277,8 @@ function migrateSettings() {
}
// This function is called when the extension is loaded
jQuery(async () => {
// This is an example of loading HTML from a file
const windowHtml = $(await $.get(`${extensionFolderPath}/window.html`));
// Append settingsHtml to extensions_settings
// extension_settings and extensions_settings2 are the left and right columns of the settings menu
// Left should be extensions that deal with system functions and right should be visual/UI related
windowHtml.find('#CFGClose').on('click', function () {
export function initCfg() {
$('#CFGClose').on('click', function () {
$("#cfgConfig").transition({
opacity: 0,
duration: 200,
@@ -295,58 +287,58 @@ jQuery(async () => {
setTimeout(function () { $('#cfgConfig').hide() }, 200);
});
windowHtml.find('#chat_cfg_guidance_scale').on('input', function() {
$('#chat_cfg_guidance_scale').on('input', function() {
const numberValue = Number($(this).val());
const success = setChatCfg(numberValue, settingType.guidance_scale);
if (success) {
$('#chat_cfg_guidance_scale_counter').text(numberValue.toFixed(2));
$('#chat_cfg_guidance_scale_counter').val(numberValue.toFixed(2));
}
});
windowHtml.find('#chat_cfg_negative_prompt').on('input', function() {
$('#chat_cfg_negative_prompt').on('input', function() {
setChatCfg($(this).val(), settingType.negative_prompt);
});
windowHtml.find('#chat_cfg_positive_prompt').on('input', function() {
$('#chat_cfg_positive_prompt').on('input', function() {
setChatCfg($(this).val(), settingType.positive_prompt);
});
windowHtml.find('#chara_cfg_guidance_scale').on('input', function() {
$('#chara_cfg_guidance_scale').on('input', function() {
const value = $(this).val();
const success = setCharCfg(value, settingType.guidance_scale);
if (success) {
$('#chara_cfg_guidance_scale_counter').text(Number(value).toFixed(2));
$('#chara_cfg_guidance_scale_counter').val(Number(value).toFixed(2));
}
});
windowHtml.find('#chara_cfg_negative_prompt').on('input', function() {
$('#chara_cfg_negative_prompt').on('input', function() {
setCharCfg($(this).val(), settingType.negative_prompt);
});
windowHtml.find('#chara_cfg_positive_prompt').on('input', function() {
$('#chara_cfg_positive_prompt').on('input', function() {
setCharCfg($(this).val(), settingType.positive_prompt);
});
windowHtml.find('#global_cfg_guidance_scale').on('input', function() {
$('#global_cfg_guidance_scale').on('input', function() {
extension_settings.cfg.global.guidance_scale = Number($(this).val());
$('#global_cfg_guidance_scale_counter').text(extension_settings.cfg.global.guidance_scale.toFixed(2));
$('#global_cfg_guidance_scale_counter').val(extension_settings.cfg.global.guidance_scale.toFixed(2));
saveSettingsDebounced();
});
windowHtml.find('#global_cfg_negative_prompt').on('input', function() {
$('#global_cfg_negative_prompt').on('input', function() {
extension_settings.cfg.global.negative_prompt = $(this).val();
saveSettingsDebounced();
});
windowHtml.find('#global_cfg_positive_prompt').on('input', function() {
$('#global_cfg_positive_prompt').on('input', function() {
extension_settings.cfg.global.positive_prompt = $(this).val();
saveSettingsDebounced();
});
windowHtml.find(`input[name="cfg_prompt_combine"]`).on('input', function() {
const values = windowHtml.find(`input[name="cfg_prompt_combine"]`)
$(`input[name="cfg_prompt_combine"]`).on('input', function() {
const values = $('#cfgConfig').find(`input[name="cfg_prompt_combine"]`)
.filter(":checked")
.map(function() { return parseInt($(this).val()) })
.map(function() { return Number($(this).val()) })
.get()
.filter((e) => !Number.isNaN(e)) || [];
@@ -354,17 +346,17 @@ jQuery(async () => {
saveMetadataDebounced();
});
windowHtml.find(`#cfg_prompt_insertion_depth`).on('input', function() {
$(`#cfg_prompt_insertion_depth`).on('input', function() {
chat_metadata[metadataKeys.prompt_insertion_depth] = Number($(this).val());
saveMetadataDebounced();
});
windowHtml.find(`#cfg_prompt_separator`).on('input', function() {
$(`#cfg_prompt_separator`).on('input', function() {
chat_metadata[metadataKeys.prompt_separator] = $(this).val();
saveMetadataDebounced();
});
windowHtml.find('#groupchat_cfg_use_chara').on('input', function() {
$('#groupchat_cfg_use_chara').on('input', function() {
const checked = !!$(this).prop('checked');
chat_metadata[metadataKeys.groupchat_individual_chars] = checked
@@ -375,20 +367,126 @@ jQuery(async () => {
saveMetadataDebounced();
});
$("#movingDivs").append(windowHtml);
initialLoadSettings();
if (extension_settings.cfg) {
migrateSettings();
}
const buttonHtml = $(await $.get(`${extensionFolderPath}/menuButton.html`));
buttonHtml.on('click', onCfgMenuItemClick)
buttonHtml.appendTo("#options_advanced");
$('#option_toggle_CFG').on('click', onCfgMenuItemClick);
// Hook events
eventSource.on(event_types.CHAT_CHANGED, async () => {
await onChatChanged();
});
});
}
export const cfgType = {
chat: 0,
chara: 1,
global: 2
}
export const metadataKeys = {
guidance_scale: "cfg_guidance_scale",
negative_prompt: "cfg_negative_prompt",
positive_prompt: "cfg_positive_prompt",
prompt_combine: "cfg_prompt_combine",
groupchat_individual_chars: "cfg_groupchat_individual_chars",
prompt_insertion_depth: "cfg_prompt_insertion_depth",
prompt_separator: "cfg_prompt_separator"
}
// Gets the CFG guidance scale
// If the guidance scale is 1, ignore the CFG prompt(s) since it won't be used anyways
export function getGuidanceScale() {
if (!extension_settings.cfg) {
console.warn("CFG extension is not enabled. Skipping CFG guidance.");
return;
}
const charaCfg = extension_settings.cfg.chara?.find((e) => e.name === getCharaFilename(this_chid));
const chatGuidanceScale = chat_metadata[metadataKeys.guidance_scale];
const groupchatCharOverride = chat_metadata[metadataKeys.groupchat_individual_chars] ?? false;
if (chatGuidanceScale && chatGuidanceScale !== 1 && !groupchatCharOverride) {
return {
type: cfgType.chat,
value: chatGuidanceScale
};
}
if ((!selected_group && charaCfg || groupchatCharOverride) && charaCfg?.guidance_scale !== 1) {
return {
type: cfgType.chara,
value: charaCfg.guidance_scale
};
}
if (extension_settings.cfg.global && extension_settings.cfg.global?.guidance_scale !== 1) {
return {
type: cfgType.global,
value: extension_settings.cfg.global.guidance_scale
};
}
}
/**
* Gets the CFG prompt separator.
* @returns {string} The CFG prompt separator
*/
function getCustomSeparator() {
const defaultSeparator = "\n";
try {
if (chat_metadata[metadataKeys.prompt_separator]) {
return JSON.parse(chat_metadata[metadataKeys.prompt_separator]);
}
return defaultSeparator;
} catch {
console.warn("Invalid JSON detected for prompt separator. Using default separator.");
return defaultSeparator;
}
}
// Gets the CFG prompt
export function getCfgPrompt(guidanceScale, isNegative) {
let splitCfgPrompt = [];
const cfgPromptCombine = chat_metadata[metadataKeys.prompt_combine] ?? [];
if (guidanceScale.type === cfgType.chat || cfgPromptCombine.includes(cfgType.chat)) {
splitCfgPrompt.unshift(
substituteParams(
chat_metadata[isNegative ? metadataKeys.negative_prompt : metadataKeys.positive_prompt]
)
);
}
const charaCfg = extension_settings.cfg.chara?.find((e) => e.name === getCharaFilename(this_chid));
if (guidanceScale.type === cfgType.chara || cfgPromptCombine.includes(cfgType.chara)) {
splitCfgPrompt.unshift(
substituteParams(
isNegative ? charaCfg.negative_prompt : charaCfg.positive_prompt
)
);
}
if (guidanceScale.type === cfgType.global || cfgPromptCombine.includes(cfgType.global)) {
splitCfgPrompt.unshift(
substituteParams(
isNegative ? extension_settings.cfg.global.negative_prompt : extension_settings.cfg.global.positive_prompt
)
);
}
const customSeparator = getCustomSeparator();
const combinedCfgPrompt = splitCfgPrompt.filter((e) => e.length > 0).join(customSeparator);
const insertionDepth = chat_metadata[metadataKeys.prompt_insertion_depth] ?? 1;
console.log(`Setting CFG with guidance scale: ${guidanceScale.value}, negatives: ${combinedCfgPrompt}`);
return {
value: combinedCfgPrompt,
depth: insertionDepth
};
}

73
public/scripts/chats.js Normal file
View File

@@ -0,0 +1,73 @@
// Move chat functions here from script.js (eventually)
import {
chat,
getCurrentChatId,
hideSwipeButtons,
saveChatConditional,
showSwipeButtons,
} from "../script.js";
/**
* Mark message as hidden (system message).
* @param {number} messageId Message ID
* @param {JQuery<Element>} messageBlock Message UI element
* @returns
*/
export async function hideChatMessage(messageId, messageBlock) {
const chatId = getCurrentChatId();
if (!chatId || isNaN(messageId)) return;
const message = chat[messageId];
if (!message) return;
message.is_system = true;
messageBlock.attr('is_system', String(true));
// Reload swipes. Useful when a last message is hidden.
hideSwipeButtons();
showSwipeButtons();
await saveChatConditional();
}
/**
* Mark message as visible (non-system message).
* @param {number} messageId Message ID
* @param {JQuery<Element>} messageBlock Message UI element
* @returns
*/
export async function unhideChatMessage(messageId, messageBlock) {
const chatId = getCurrentChatId();
if (!chatId || isNaN(messageId)) return;
const message = chat[messageId];
if (!message) return;
message.is_system = false;
messageBlock.attr('is_system', String(false));
// Reload swipes. Useful when a last message is hidden.
hideSwipeButtons();
showSwipeButtons();
await saveChatConditional();
}
jQuery(function() {
$(document).on('click', '.mes_hide', async function() {
const messageBlock = $(this).closest('.mes');
const messageId = Number(messageBlock.attr('mesid'));
await hideChatMessage(messageId, messageBlock);
});
$(document).on('click', '.mes_unhide', async function() {
const messageBlock = $(this).closest('.mes');
const messageId = Number(messageBlock.attr('mesid'));
await unhideChatMessage(messageId, messageBlock);
});
})

View File

@@ -1,4 +1,5 @@
import { callPopup, eventSource, event_types, saveSettings, saveSettingsDebounced, getRequestHeaders, substituteParams, renderTemplate } from "../script.js";
import { hideLoader, showLoader } from "./loader.js";
import { isSubsetOf } from "./utils.js";
export {
getContext,
@@ -11,7 +12,7 @@ export {
ModuleWorkerWrapper,
};
let extensionNames = [];
export let extensionNames = [];
let manifests = {};
const defaultUrl = "http://localhost:5100";
@@ -123,6 +124,7 @@ const extension_settings = {
apiUrl: defaultUrl,
apiKey: '',
autoConnect: false,
notifyUpdates: false,
disabledExtensions: [],
expressionOverrides: [],
memory: {},
@@ -158,6 +160,9 @@ const extension_settings = {
rvc: {},
hypebot: {},
vectors: {},
variables: {
global: {},
},
};
let modules = [];
@@ -367,6 +372,15 @@ function addExtensionsButtonAndMenu() {
});
}
function notifyUpdatesInputHandler() {
extension_settings.notifyUpdates = !!$('#extensions_notify_updates').prop('checked');
saveSettingsDebounced();
if (extension_settings.notifyUpdates) {
checkForExtensionUpdates(true);
}
}
/* $(document).on('click', function (e) {
const target = $(e.target);
if (target.is(dropdown)) return;
@@ -569,7 +583,7 @@ async function getExtensionData(extension) {
function getModuleInformation() {
let moduleInfo = modules.length ? `<p>${DOMPurify.sanitize(modules.join(', '))}</p>` : '<p class="failure">Not connected to the API!</p>';
return `
<h3>Modules provided by your Extensions API:</h3>
<h3>Modules provided by your Extras API:</h3>
${moduleInfo}
`;
}
@@ -578,26 +592,43 @@ function getModuleInformation() {
* Generates the HTML strings for all extensions and displays them in a popup.
*/
async function showExtensionsDetails() {
let htmlDefault = '<h3>Default Extensions:</h3>';
let htmlExternal = '<h3>External Extensions:</h3>';
try{
showLoader();
let htmlDefault = '<h3>Built-in Extensions:</h3>';
let htmlExternal = '<h3>Installed Extensions:</h3>';
const extensions = Object.entries(manifests).sort((a, b) => a[1].loading_order - b[1].loading_order);
const extensions = Object.entries(manifests).sort((a, b) => a[1].loading_order - b[1].loading_order);
const promises = [];
for (const extension of extensions) {
const { isExternal, extensionHtml } = await getExtensionData(extension);
if (isExternal) {
htmlExternal += extensionHtml;
} else {
htmlDefault += extensionHtml;
for (const extension of extensions) {
promises.push(getExtensionData(extension));
}
}
const html = `
${getModuleInformation()}
${htmlDefault}
${htmlExternal}
`;
callPopup(`<div class="extensions_info">${html}</div>`, 'text');
const settledPromises = await Promise.allSettled(promises);
settledPromises.forEach(promise => {
if (promise.status === 'fulfilled') {
const { isExternal, extensionHtml } = promise.value;
if (isExternal) {
htmlExternal += extensionHtml;
} else {
htmlDefault += extensionHtml;
}
}
});
const html = `
${getModuleInformation()}
${htmlDefault}
${htmlExternal}
`;
callPopup(`<div class="extensions_info">${html}</div>`, 'text');
} catch (error) {
toastr.error('Error loading extensions. See browser console for details.');
console.error(error);
} finally {
hideLoader();
}
}
@@ -609,6 +640,15 @@ async function showExtensionsDetails() {
*/
async function onUpdateClick() {
const extensionName = $(this).data('name');
await updateExtension(extensionName, false);
}
/**
* Updates a third-party extension via the API.
* @param {string} extensionName Extension folder name
* @param {boolean} quiet If true, don't show a success message
*/
async function updateExtension(extensionName, quiet) {
try {
const response = await fetch('/api/extensions/update', {
method: 'POST',
@@ -618,15 +658,20 @@ async function onUpdateClick() {
const data = await response.json();
if (data.isUpToDate) {
toastr.success('Extension is already up to date');
if (!quiet) {
toastr.success('Extension is already up to date');
}
} else {
toastr.success(`Extension updated to ${data.shortCommitHash}`);
toastr.success(`Extension ${extensionName} updated to ${data.shortCommitHash}`);
}
if (!quiet) {
showExtensionsDetails();
}
showExtensionsDetails();
} catch (error) {
console.error('Error:', error);
}
};
}
/**
* Handles the click event for the delete button of an extension.
@@ -639,23 +684,26 @@ async function onDeleteClick() {
// use callPopup to create a popup for the user to confirm before delete
const confirmation = await callPopup(`Are you sure you want to delete ${extensionName}?`, 'delete_extension');
if (confirmation) {
try {
const response = await fetch('/api/extensions/delete', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ extensionName })
});
} catch (error) {
console.error('Error:', error);
}
toastr.success(`Extension ${extensionName} deleted`);
showExtensionsDetails();
// reload the page to remove the extension from the list
location.reload();
await deleteExtension(extensionName);
}
};
export async function deleteExtension(extensionName) {
try {
const response = await fetch('/api/extensions/delete', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ extensionName })
});
} catch (error) {
console.error('Error:', error);
}
toastr.success(`Extension ${extensionName} deleted`);
showExtensionsDetails();
// reload the page to remove the extension from the list
location.reload();
}
/**
* Fetches the version details of a specific extension.
@@ -680,9 +728,42 @@ async function getExtensionVersion(extensionName) {
}
}
/**
* Installs a third-party extension via the API.
* @param {string} url Extension repository URL
* @returns {Promise<void>}
*/
export async function installExtension(url) {
console.debug('Extension installation started', url);
toastr.info('Please wait...', 'Installing extension');
async function loadExtensionSettings(settings) {
const request = await fetch('/api/extensions/install', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ url }),
});
if (!request.ok) {
const text = await request.text();
toastr.warning(text || request.statusText, 'Extension installation failed', { timeOut: 5000 });
console.error('Extension installation failed', request.status, request.statusText, text);
return;
}
const response = await request.json();
toastr.success(`Extension "${response.display_name}" by ${response.author} (version ${response.version}) has been installed successfully!`, 'Extension installation successful');
console.debug(`Extension "${response.display_name}" has been installed successfully at ${response.extensionPath}`);
await loadExtensionSettings({}, false);
eventSource.emit(event_types.EXTENSION_SETTINGS_LOADED);
}
/**
* Loads extension settings from the app settings.
* @param {object} settings App Settings
* @param {boolean} versionChanged Is this a version change?
*/
async function loadExtensionSettings(settings, versionChanged) {
if (settings.extension_settings) {
Object.assign(extension_settings, settings.extension_settings);
}
@@ -690,28 +771,117 @@ async function loadExtensionSettings(settings) {
$("#extensions_url").val(extension_settings.apiUrl);
$("#extensions_api_key").val(extension_settings.apiKey);
$("#extensions_autoconnect").prop('checked', extension_settings.autoConnect);
$("#extensions_notify_updates").prop('checked', extension_settings.notifyUpdates);
// Activate offline extensions
eventSource.emit(event_types.EXTENSIONS_FIRST_LOAD);
extensionNames = await discoverExtensions();
manifests = await getManifests(extensionNames)
if (versionChanged) {
await autoUpdateExtensions();
}
await activateExtensions();
if (extension_settings.autoConnect && extension_settings.apiUrl) {
connectToApi(extension_settings.apiUrl);
}
}
export function doDailyExtensionUpdatesCheck() {
setTimeout(() => {
if (extension_settings.notifyUpdates) {
checkForExtensionUpdates(false);
}
}, 1);
}
/**
* Checks if there are updates available for 3rd-party extensions.
* @param {boolean} force Skip nag check
* @returns {Promise<any>}
*/
async function checkForExtensionUpdates(force) {
if (!force) {
const STORAGE_NAG_KEY = 'extension_update_nag';
const currentDate = new Date().toDateString();
// Don't nag more than once a day
if (localStorage.getItem(STORAGE_NAG_KEY) === currentDate) {
return;
}
localStorage.setItem(STORAGE_NAG_KEY, currentDate);
}
const updatesAvailable = [];
const promises = [];
for (const [id, manifest] of Object.entries(manifests)) {
if (manifest.auto_update && id.startsWith('third-party')) {
const promise = new Promise(async (resolve, reject) => {
try {
const data = await getExtensionVersion(id.replace('third-party', ''));
if (data.isUpToDate === false) {
updatesAvailable.push(manifest.display_name);
}
resolve();
} catch (error) {
console.error('Error checking for extension updates', error);
reject();
}
});
promises.push(promise);
}
}
await Promise.allSettled(promises);
if (updatesAvailable.length > 0) {
toastr.info(`${updatesAvailable.map(x => `${x}`).join('\n')}`, 'Extension updates available');
}
}
async function autoUpdateExtensions() {
for (const [id, manifest] of Object.entries(manifests)) {
if (manifest.auto_update && id.startsWith('third-party')) {
console.debug(`Auto-updating 3rd-party extension: ${manifest.display_name} (${id})`);
await updateExtension(id.replace('third-party', ''), true);
}
}
}
/**
* Runs the generate interceptors for all extensions.
* @param {any[]} chat Chat array
* @param {number} contextSize Context size
* @returns {Promise<boolean>} True if generation should be aborted
*/
async function runGenerationInterceptors(chat, contextSize) {
let aborted = false;
let exitImmediately = false;
const abort = (/** @type {boolean} */ immediately) => {
aborted = true;
exitImmediately = immediately;
};
for (const manifest of Object.values(manifests)) {
const interceptorKey = manifest.generate_interceptor;
if (typeof window[interceptorKey] === 'function') {
try {
await window[interceptorKey](chat, contextSize);
await window[interceptorKey](chat, contextSize, abort);
} catch (e) {
console.error(`Failed running interceptor for ${manifest.display_name}`, e);
}
}
if (exitImmediately) {
break;
}
}
return aborted;
}
jQuery(function () {
@@ -721,8 +891,36 @@ jQuery(function () {
$("#extensions_connect").on('click', connectClickHandler);
$("#extensions_autoconnect").on('input', autoConnectInputHandler);
$("#extensions_details").on('click', showExtensionsDetails);
$("#extensions_notify_updates").on('input', notifyUpdatesInputHandler);
$(document).on('click', '.toggle_disable', onDisableExtensionClick);
$(document).on('click', '.toggle_enable', onEnableExtensionClick);
$(document).on('click', '.btn_update', onUpdateClick);
$(document).on('click', '.btn_delete', onDeleteClick);
/**
* Handles the click event for the third-party extension import button.
* Prompts the user to enter the Git URL of the extension to import.
* After obtaining the Git URL, makes a POST request to '/api/extensions/install' to import the extension.
* If the extension is imported successfully, a success message is displayed.
* If the extension import fails, an error message is displayed and the error is logged to the console.
* After successfully importing the extension, the extension settings are reloaded and a 'EXTENSION_SETTINGS_LOADED' event is emitted.
*
* @listens #third_party_extension_button#click - The click event of the '#third_party_extension_button' element.
*/
$('#third_party_extension_button').on('click', async () => {
const html = `<h3>Enter the Git URL of the extension to install</h3>
<br>
<p><b>Disclaimer:</b> Please be aware that using external extensions can have unintended side effects and may pose security risks. Always make sure you trust the source before importing an extension. We are not responsible for any damage caused by third-party extensions.</p>
<br>
<p>Example: <tt> https://github.com/author/extension-name </tt></p>`
const input = await callPopup(html, 'input');
if (!input) {
console.debug('Extension install cancelled');
return;
}
const url = input.trim();
await installExtension(url);
});
});

View File

@@ -0,0 +1,9 @@
<div class="m-b-1">
Are you sure you want to connect to '{{url}}'?
</div>
<div class="flex-container justifyCenter">
<label class="checkbox_label" for="assets-remember">
<input type="checkbox" id="assets-remember">
Don't ask again for this URL
</label>
</div>

View File

@@ -1,14 +1,16 @@
/*
TODO:
- Check failed install file (0kb size ?)
*/
//const DEBUG_TONY_SAMA_FORK_MODE = false
//const DEBUG_TONY_SAMA_FORK_MODE = true
import { getRequestHeaders, callPopup } from "../../../script.js";
import { deleteExtension, extensionNames, installExtension, renderExtensionTemplate } from "../../extensions.js";
import { getStringHash, isValidUrl } from "../../utils.js";
export { MODULE_NAME };
const MODULE_NAME = 'Assets';
const MODULE_NAME = 'assets';
const DEBUG_PREFIX = "<Assets module> ";
let previewAudio = null;
let ASSETS_JSON_URL = "https://raw.githubusercontent.com/SillyTavern/SillyTavern-Content/main/index.json"
const extensionName = "assets";
@@ -29,7 +31,7 @@ const defaultSettings = {
function downloadAssetsList(url) {
updateCurrentAssets().then(function () {
fetch(url)
fetch(url, { cache: "no-cache" })
.then(response => response.json())
.then(json => {
@@ -47,10 +49,20 @@ function downloadAssetsList(url) {
}
console.debug(DEBUG_PREFIX, "Updated available assets to", availableAssets);
// First extensions, then everything else
const assetTypes = Object.keys(availableAssets).sort((a, b) => (a === 'extension') ? -1 : (b === 'extension') ? 1 : 0);
for (const assetType in availableAssets) {
for (const assetType of assetTypes) {
let assetTypeMenu = $('<div />', { id: "assets_audio_ambient_div", class: "assets-list-div" });
assetTypeMenu.append(`<h3>${assetType}</h3>`)
if (assetType == 'extension') {
assetTypeMenu.append(`
<div class="assets-list-git">
To download extensions from this page, you need to have <a href="https://git-scm.com/downloads" target="_blank">Git</a> installed.
</div>`);
}
for (const i in availableAssets[assetType]) {
const asset = availableAssets[assetType][i];
const elemId = `assets_install_${assetType}_${i}`;
@@ -59,7 +71,7 @@ function downloadAssetsList(url) {
element.append(label);
//if (DEBUG_TONY_SAMA_FORK_MODE)
// assetUrl = assetUrl.replace("https://github.com/SillyTavern/","https://github.com/Tony-sama/"); // DBG
// asset["url"] = asset["url"].replace("https://github.com/SillyTavern/","https://github.com/Tony-sama/"); // DBG
console.debug(DEBUG_PREFIX, "Checking asset", asset["id"], asset["url"]);
@@ -71,18 +83,18 @@ function downloadAssetsList(url) {
label.addClass("fa-check");
this.classList.remove('asset-download-button-loading');
element.on("click", assetDelete);
element.on("mouseenter", function(){
element.on("mouseenter", function () {
label.removeClass("fa-check");
label.addClass("fa-trash");
label.addClass("redOverlayGlow");
}).on("mouseleave", function(){
}).on("mouseleave", function () {
label.addClass("fa-check");
label.removeClass("fa-trash");
label.removeClass("redOverlayGlow");
});
};
const assetDelete = async function() {
const assetDelete = async function () {
element.off("click");
await deleteAsset(assetType, asset["id"]);
label.removeClass("fa-check");
@@ -98,11 +110,11 @@ function downloadAssetsList(url) {
label.toggleClass("fa-download");
label.toggleClass("fa-check");
element.on("click", assetDelete);
element.on("mouseenter", function(){
element.on("mouseenter", function () {
label.removeClass("fa-check");
label.addClass("fa-trash");
label.addClass("redOverlayGlow");
}).on("mouseleave", function(){
}).on("mouseleave", function () {
label.addClass("fa-check");
label.removeClass("fa-trash");
label.removeClass("redOverlayGlow");
@@ -114,14 +126,28 @@ function downloadAssetsList(url) {
element.on("click", assetInstall);
}
console.debug(DEBUG_PREFIX, "Created element for BGM", asset["id"])
console.debug(DEBUG_PREFIX, "Created element for ", asset["id"])
const displayName = DOMPurify.sanitize(asset["name"] || asset["id"]);
const description = DOMPurify.sanitize(asset["description"] || "");
const url = isValidUrl(asset["url"]) ? asset["url"] : "";
const previewIcon = assetType == 'extension' ? 'fa-arrow-up-right-from-square' : 'fa-headphones-simple';
$(`<i></i>`)
.append(element)
.append(`<span>${asset["id"]}</span>`)
.append(`<div class="flex-container flexFlowColumn">
<span class="flex-container alignitemscenter">
<b>${displayName}</b>
<a class="asset_preview" href="${url}" target="_blank" title="Preview in browser">
<i class="fa-solid fa-sm ${previewIcon}"></i>
</a>
</span>
<span>${description}</span>
</div>`)
.appendTo(assetTypeMenu);
}
assetTypeMenu.appendTo("#assets_menu");
assetTypeMenu.on('click', 'a.asset_preview', previewAsset);
}
$("#assets_menu").show();
@@ -135,8 +161,37 @@ function downloadAssetsList(url) {
});
}
function previewAsset(e) {
const href = $(this).attr('href');
const audioExtensions = ['.mp3', '.ogg', '.wav'];
if (audioExtensions.some(ext => href.endsWith(ext))) {
e.preventDefault();
if (previewAudio) {
previewAudio.pause();
if (previewAudio.src === href) {
previewAudio = null;
return;
}
}
previewAudio = new Audio(href);
previewAudio.play();
return;
}
}
function isAssetInstalled(assetType, filename) {
for (const i of currentAssets[assetType]) {
let assetList = currentAssets[assetType];
if (assetType == 'extension') {
const thirdPartyMarker = "third-party/";
assetList = extensionNames.filter(x => x.startsWith(thirdPartyMarker)).map(x => x.replace(thirdPartyMarker, ''));
}
for (const i of assetList) {
//console.debug(DEBUG_PREFIX,i,filename)
if (i.includes(filename))
return true;
@@ -149,6 +204,13 @@ async function installAsset(url, assetType, filename) {
console.debug(DEBUG_PREFIX, "Downloading ", url);
const category = assetType;
try {
if (category === 'extension') {
console.debug(DEBUG_PREFIX, "Installing extension ", url)
await installExtension(url);
console.debug(DEBUG_PREFIX, "Extension installed.")
return;
}
const body = { url, category, filename };
const result = await fetch('/api/assets/download', {
method: 'POST',
@@ -170,6 +232,12 @@ async function deleteAsset(assetType, filename) {
console.debug(DEBUG_PREFIX, "Deleting ", assetType, filename);
const category = assetType;
try {
if (category === 'extension') {
console.debug(DEBUG_PREFIX, "Deleting extension ", filename)
await deleteExtension(filename);
console.debug(DEBUG_PREFIX, "Extension deleted.")
}
const body = { category, filename };
const result = await fetch('/api/assets/delete', {
method: 'POST',
@@ -214,24 +282,35 @@ async function updateCurrentAssets() {
// This function is called when the extension is loaded
jQuery(async () => {
// This is an example of loading HTML from a file
const windowHtml = $(await $.get(`${extensionFolderPath}/window.html`));
const windowHtml = $(renderExtensionTemplate(MODULE_NAME, 'window', {}));
const assetsJsonUrl = windowHtml.find('#assets-json-url-field');
assetsJsonUrl.val(ASSETS_JSON_URL);
const connectButton = windowHtml.find('#assets-connect-button');
connectButton.on("click", async function () {
const confirmation = await callPopup(`Are you sure you want to connect to '${assetsJsonUrl.val()}'?`, 'confirm')
const url = String(assetsJsonUrl.val());
const rememberKey = `Assets_SkipConfirm_${getStringHash(url)}`;
const skipConfirm = localStorage.getItem(rememberKey) === 'true';
const template = renderExtensionTemplate(MODULE_NAME, 'confirm', { url });
const confirmation = skipConfirm || await callPopup(template, 'confirm');
if (confirmation) {
try {
if (!skipConfirm) {
const rememberValue = Boolean($('#assets-remember').prop('checked'));
localStorage.setItem(rememberKey, String(rememberValue));
}
console.debug(DEBUG_PREFIX, "Confimation, loading assets...");
downloadAssetsList(assetsJsonUrl.val());
downloadAssetsList(url);
connectButton.removeClass("fa-plug-circle-exclamation");
connectButton.removeClass("redOverlayGlow");
connectButton.addClass("fa-plug-circle-check");
} catch (error) {
console.error('Error:', error);
toastr.error(`Cannot get assets list from ${assetsJsonUrl.val()}`);
toastr.error(`Cannot get assets list from ${url}`);
connectButton.removeClass("fa-plug-circle-check");
connectButton.addClass("fa-plug-circle-exclamation");
connectButton.removeClass("redOverlayGlow");

View File

@@ -13,18 +13,37 @@
padding: 5px;
}
.assets-list-git {
font-size: calc(var(--mainFontSize) * 0.8);
opacity: 0.8;
margin-bottom: 1em;
}
.assets-list-div h3 {
text-transform: capitalize;
}
.assets-list-div i a {
color: inherit;
}
.assets-list-div i {
display: flex;
flex-direction: row;
align-items: center;
justify-content: left;
padding: 5px;
font-style: normal;
}
.assets-list-div i span{
.assets-list-div i span {
margin-left: 10px;
}
.assets-list-div i span:first-of-type {
font-weight: bold;
}
.asset-download-button {
position: relative;
width: 50px;
@@ -33,8 +52,8 @@
outline: none;
border-radius: 2px;
cursor: pointer;
}
}
.asset-download-button:active {
background: #007a63;
}
@@ -67,13 +86,11 @@
}
@keyframes asset-download-button-loading-spinner {
from {
transform: rotate(0turn);
}
from {
transform: rotate(0turn);
}
to {
transform: rotate(1turn);
to {
transform: rotate(1turn);
}
}
}

View File

@@ -1,7 +1,7 @@
<div id="assets_ui">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>Assets</b>
<b>Download Extensions & Assets</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">

View File

@@ -1,917 +0,0 @@
/*
Ideas:
- Clean design of new ui
- change select text versus options for playing: audio
- cross fading between bgm / start a different time
- fading should appear before end when switching randomly
- Background based ambient sounds
- import option on background UI ?
- Allow background music edition using background menu
- https://fontawesome.com/icons/music?f=classic&s=solid
- https://codepen.io/noirsociety/pen/rNQxQwm
- https://codepen.io/xrocker/pen/abdKVGy
*/
import { saveSettingsDebounced, getRequestHeaders } from "../../../script.js";
import { getContext, extension_settings, ModuleWorkerWrapper } from "../../extensions.js";
import { isDataURL } from "../../utils.js";
export { MODULE_NAME };
const extensionName = "audio";
const extensionFolderPath = `scripts/extensions/${extensionName}`;
const MODULE_NAME = 'Audio';
const DEBUG_PREFIX = "<Audio module> ";
const UPDATE_INTERVAL = 1000;
const ASSETS_BGM_FOLDER = "bgm";
const ASSETS_AMBIENT_FOLDER = "ambient";
const CHARACTER_BGM_FOLDER = "bgm"
const FALLBACK_EXPRESSION = "neutral";
const DEFAULT_EXPRESSIONS = [
//"talkinghead",
"admiration",
"amusement",
"anger",
"annoyance",
"approval",
"caring",
"confusion",
"curiosity",
"desire",
"disappointment",
"disapproval",
"disgust",
"embarrassment",
"excitement",
"fear",
"gratitude",
"grief",
"joy",
"love",
"nervousness",
"optimism",
"pride",
"realization",
"relief",
"remorse",
"sadness",
"surprise",
"neutral"
];
const SPRITE_DOM_ID = "#expression-image";
let current_chat_id = null
let fallback_BGMS = null; // Initialized only once with module workers
let ambients = null; // Initialized only once with module workers
let characterMusics = {}; // Updated with module workers
let currentCharacterBGM = null;
let currentExpressionBGM = null;
let currentBackground = null;
let cooldownBGM = 0;
let bgmEnded = true;
//#############################//
// Extension UI and Settings //
//#############################//
const defaultSettings = {
enabled: false,
dynamic_bgm_enabled: false,
//dynamic_ambient_enabled: false,
bgm_locked: true,
bgm_muted: true,
bgm_volume: 50,
bgm_selected: null,
ambient_locked: true,
ambient_muted: true,
ambient_volume: 50,
ambient_selected: null,
bgm_cooldown: 30
}
function loadSettings() {
if (extension_settings.audio === undefined)
extension_settings.audio = {};
if (Object.keys(extension_settings.audio).length === 0) {
Object.assign(extension_settings.audio, defaultSettings)
}
$("#audio_enabled").prop('checked', extension_settings.audio.enabled);
$("#audio_dynamic_bgm_enabled").prop('checked', extension_settings.audio.dynamic_bgm_enabled);
//$("#audio_dynamic_ambient_enabled").prop('checked', extension_settings.audio.dynamic_ambient_enabled);
$("#audio_bgm_volume").text(extension_settings.audio.bgm_volume);
$("#audio_ambient_volume").text(extension_settings.audio.ambient_volume);
$("#audio_bgm_volume_slider").val(extension_settings.audio.bgm_volume);
$("#audio_ambient_volume_slider").val(extension_settings.audio.ambient_volume);
if (extension_settings.audio.bgm_muted) {
$("#audio_bgm_mute_icon").removeClass("fa-volume-high");
$("#audio_bgm_mute_icon").addClass("fa-volume-mute");
$("#audio_bgm_mute").addClass("redOverlayGlow");
$("#audio_bgm").prop("muted", true);
}
else {
$("#audio_bgm_mute_icon").addClass("fa-volume-high");
$("#audio_bgm_mute_icon").removeClass("fa-volume-mute");
$("#audio_bgm_mute").removeClass("redOverlayGlow");
$("#audio_bgm").prop("muted", false);
}
if (extension_settings.audio.bgm_locked) {
//$("#audio_bgm_lock_icon").removeClass("fa-lock-open");
//$("#audio_bgm_lock_icon").addClass("fa-lock");
$("#audio_bgm").attr("loop", true);
$("#audio_bgm_lock").addClass("redOverlayGlow");
}
else {
//$("#audio_bgm_lock_icon").removeClass("fa-lock");
//$("#audio_bgm_lock_icon").addClass("fa-lock-open");
$("#audio_bgm").attr("loop", false);
$("#audio_bgm_lock").removeClass("redOverlayGlow");
}
/*
if (extension_settings.audio.bgm_selected !== null) {
$("#audio_bgm_select").append(new Option(extension_settings.audio.bgm_selected, extension_settings.audio.bgm_selected));
$("#audio_bgm_select").val(extension_settings.audio.bgm_selected);
}*/
if (extension_settings.audio.ambient_locked) {
$("#audio_ambient_lock_icon").removeClass("fa-lock-open");
$("#audio_ambient_lock_icon").addClass("fa-lock");
$("#audio_ambient_lock").addClass("redOverlayGlow");
}
else {
$("#audio_ambient_lock_icon").removeClass("fa-lock");
$("#audio_ambient_lock_icon").addClass("fa-lock-open");
}
/*
if (extension_settings.audio.ambient_selected !== null) {
$("#audio_ambient_select").append(new Option(extension_settings.audio.ambient_selected, extension_settings.audio.ambient_selected));
$("#audio_ambient_select").val(extension_settings.audio.ambient_selected);
}*/
if (extension_settings.audio.ambient_muted) {
$("#audio_ambient_mute_icon").removeClass("fa-volume-high");
$("#audio_ambient_mute_icon").addClass("fa-volume-mute");
$("#audio_ambient_mute").addClass("redOverlayGlow");
$("#audio_ambient").prop("muted", true);
}
else {
$("#audio_ambient_mute_icon").addClass("fa-volume-high");
$("#audio_ambient_mute_icon").removeClass("fa-volume-mute");
$("#audio_ambient_mute").removeClass("redOverlayGlow");
$("#audio_ambient").prop("muted", false);
}
$("#audio_bgm_cooldown").val(extension_settings.audio.bgm_cooldown);
$("#audio_debug_div").hide(); // DBG: comment to see debug mode
}
async function onEnabledClick() {
extension_settings.audio.enabled = $('#audio_enabled').is(':checked');
if (extension_settings.audio.enabled) {
if ($("#audio_bgm").attr("src") != "")
$("#audio_bgm")[0].play();
if ($("#audio_ambient").attr("src") != "")
$("#audio_ambient")[0].play();
} else {
$("#audio_bgm")[0].pause();
$("#audio_ambient")[0].pause();
}
saveSettingsDebounced();
}
async function onDynamicBGMEnabledClick() {
extension_settings.audio.dynamic_bgm_enabled = $('#audio_dynamic_bgm_enabled').is(':checked');
currentCharacterBGM = null;
currentExpressionBGM = null;
cooldownBGM = 0;
saveSettingsDebounced();
}
/*
async function onDynamicAmbientEnabledClick() {
extension_settings.audio.dynamic_ambient_enabled = $('#audio_dynamic_ambient_enabled').is(':checked');
currentBackground = null;
saveSettingsDebounced();
}
*/
async function onBGMLockClick() {
extension_settings.audio.bgm_locked = !extension_settings.audio.bgm_locked;
if (extension_settings.audio.bgm_locked) {
extension_settings.audio.bgm_selected = $("#audio_bgm_select").val();
$("#audio_bgm").attr("loop", true);
}
else {
$("#audio_bgm").attr("loop", false);
}
//$("#audio_bgm_lock_icon").toggleClass("fa-lock");
//$("#audio_bgm_lock_icon").toggleClass("fa-lock-open");
$("#audio_bgm_lock").toggleClass("redOverlayGlow");
saveSettingsDebounced();
}
async function onBGMRandomClick() {
var select = document.getElementById('audio_bgm_select');
var items = select.getElementsByTagName('option');
if (items.length < 2)
return;
var index;
do {
index = Math.floor(Math.random() * items.length);
} while (index == select.selectedIndex);
select.selectedIndex = index;
onBGMSelectChange();
}
async function onBGMMuteClick() {
extension_settings.audio.bgm_muted = !extension_settings.audio.bgm_muted;
$("#audio_bgm_mute_icon").toggleClass("fa-volume-high");
$("#audio_bgm_mute_icon").toggleClass("fa-volume-mute");
$("#audio_bgm").prop("muted", !$("#audio_bgm").prop("muted"));
$("#audio_bgm_mute").toggleClass("redOverlayGlow");
saveSettingsDebounced();
}
async function onAmbientLockClick() {
extension_settings.audio.ambient_locked = !extension_settings.audio.ambient_locked;
if (extension_settings.audio.ambient_locked)
extension_settings.audio.ambient_selected = $("#audio_ambient_select").val();
else {
extension_settings.audio.ambient_selected = null;
currentBackground = null;
}
$("#audio_ambient_lock_icon").toggleClass("fa-lock");
$("#audio_ambient_lock_icon").toggleClass("fa-lock-open");
$("#audio_ambient_lock").toggleClass("redOverlayGlow");
saveSettingsDebounced();
}
async function onAmbientMuteClick() {
extension_settings.audio.ambient_muted = !extension_settings.audio.ambient_muted;
$("#audio_ambient_mute_icon").toggleClass("fa-volume-high");
$("#audio_ambient_mute_icon").toggleClass("fa-volume-mute");
$("#audio_ambient").prop("muted", !$("#audio_ambient").prop("muted"));
$("#audio_ambient_mute").toggleClass("redOverlayGlow");
saveSettingsDebounced();
}
async function onBGMVolumeChange() {
extension_settings.audio.bgm_volume = ~~($("#audio_bgm_volume_slider").val());
$("#audio_bgm").prop("volume", extension_settings.audio.bgm_volume * 0.01);
$("#audio_bgm_volume").text(extension_settings.audio.bgm_volume);
saveSettingsDebounced();
//console.debug(DEBUG_PREFIX,"UPDATED BGM MAX TO",extension_settings.audio.bgm_volume);
}
async function onAmbientVolumeChange() {
extension_settings.audio.ambient_volume = ~~($("#audio_ambient_volume_slider").val());
$("#audio_ambient").prop("volume", extension_settings.audio.ambient_volume * 0.01);
$("#audio_ambient_volume").text(extension_settings.audio.ambient_volume);
saveSettingsDebounced();
//console.debug(DEBUG_PREFIX,"UPDATED Ambient MAX TO",extension_settings.audio.ambient_volume);
}
async function onBGMSelectChange() {
extension_settings.audio.bgm_selected = $("#audio_bgm_select").val();
updateBGM(true);
saveSettingsDebounced();
//console.debug(DEBUG_PREFIX,"UPDATED BGM MAX TO",extension_settings.audio.bgm_volume);
}
async function onAmbientSelectChange() {
extension_settings.audio.ambient_selected = $("#audio_ambient_select").val();
updateAmbient(true);
saveSettingsDebounced();
//console.debug(DEBUG_PREFIX,"UPDATED BGM MAX TO",extension_settings.audio.bgm_volume);
}
async function onBGMCooldownInput() {
extension_settings.audio.bgm_cooldown = ~~($("#audio_bgm_cooldown").val());
cooldownBGM = extension_settings.audio.bgm_cooldown * 1000;
saveSettingsDebounced();
console.debug(DEBUG_PREFIX, "UPDATED BGM cooldown to", extension_settings.audio.bgm_cooldown);
}
//#############################//
// API Calls //
//#############################//
async function getAssetsList(type) {
console.debug(DEBUG_PREFIX, "getting assets of type", type);
try {
const result = await fetch(`/api/assets/get`, {
method: 'POST',
headers: getRequestHeaders(),
});
const assets = result.ok ? (await result.json()) : { type: [] };
console.debug(DEBUG_PREFIX, "Found assets:", assets);
return assets[type];
}
catch (err) {
console.log(err);
return [];
}
}
async function getCharacterBgmList(name) {
console.debug(DEBUG_PREFIX, "getting bgm list for", name);
try {
const result = await fetch(`/api/assets/character?name=${encodeURIComponent(name)}&category=${CHARACTER_BGM_FOLDER}`, {
method: 'POST',
headers: getRequestHeaders(),
});
let musics = result.ok ? (await result.json()) : [];
return musics;
}
catch (err) {
console.log(err);
return [];
}
}
//#############################//
// Module Worker //
//#############################//
function fillBGMSelect() {
let found_last_selected_bgm = false;
// Update bgm list in UI
$("#audio_bgm_select")
.find('option')
.remove();
for (const file of fallback_BGMS) {
$('#audio_bgm_select').append(new Option("asset: " + file.replace(/^.*[\\\/]/, '').replace(/\.[^/.]+$/, ""), file));
if (file === extension_settings.audio.bgm_selected) {
$('#audio_bgm_select').val(extension_settings.audio.bgm_selected);
found_last_selected_bgm = true;
}
}
// Update bgm list in UI
for (const char in characterMusics)
for (const e in characterMusics[char])
for (const file of characterMusics[char][e]) {
$('#audio_bgm_select').append(new Option(char + ": " + file.replace(/^.*[\\\/]/, '').replace(/\.[^/.]+$/, ""), file));
if (file === extension_settings.audio.bgm_selected) {
$('#audio_bgm_select').val(extension_settings.audio.bgm_selected);
found_last_selected_bgm = true;
}
}
if (!found_last_selected_bgm) {
$('#audio_bgm_select').val($("#audio_bgm_select option:first").val());
extension_settings.audio.bgm_selected = null;
}
}
/*
- Update ambient sound
- Update character BGM
- Solo dynamique expression
- Group only neutral bgm
*/
async function moduleWorker() {
const moduleEnabled = extension_settings.audio.enabled;
if (moduleEnabled) {
if (cooldownBGM > 0)
cooldownBGM -= UPDATE_INTERVAL;
if (fallback_BGMS == null) {
console.debug(DEBUG_PREFIX, "Updating audio bgm assets...");
fallback_BGMS = await getAssetsList(ASSETS_BGM_FOLDER);
fallback_BGMS = fallback_BGMS.filter((filename) => filename != ".placeholder")
console.debug(DEBUG_PREFIX, "Detected assets:", fallback_BGMS);
fillBGMSelect();
}
if (ambients == null) {
console.debug(DEBUG_PREFIX, "Updating audio ambient assets...");
ambients = await getAssetsList(ASSETS_AMBIENT_FOLDER);
ambients = ambients.filter((filename) => filename != ".placeholder")
console.debug(DEBUG_PREFIX, "Detected assets:", ambients);
// Update bgm list in UI
$("#audio_ambient_select")
.find('option')
.remove();
if (extension_settings.audio.ambient_selected !== null) {
let ambient_label = extension_settings.audio.ambient_selected;
if (ambient_label.includes("assets"))
ambient_label = "asset: " + ambient_label.replace(/^.*[\\\/]/, '').replace(/\.[^/.]+$/, "");
else {
ambient_label = ambient_label.substring("/characters/".length);
ambient_label = ambient_label.substring(0, ambient_label.indexOf("/")) + ": " + ambient_label.substring(ambient_label.indexOf("/") + "/bgm/".length);
ambient_label = ambient_label.replace(/\.[^/.]+$/, "");
}
$('#audio_ambient_select').append(new Option(ambient_label, extension_settings.audio.ambient_selected));
}
for (const file of ambients) {
if (file !== extension_settings.audio.ambient_selected)
$("#audio_ambient_select").append(new Option("asset: " + file.replace(/^.*[\\\/]/, '').replace(/\.[^/.]+$/, ""), file));
}
}
// 1) Update ambient audio
// ---------------------------
//if (extension_settings.audio.dynamic_ambient_enabled) {
let newBackground = $("#bg1").css("background-image");
const custom_background = getContext()["chatMetadata"]["custom_background"];
if (custom_background !== undefined)
newBackground = custom_background
if (!isDataURL(newBackground)) {
newBackground = newBackground.substring(newBackground.lastIndexOf("/") + 1).replace(/\.[^/.]+$/, "").replaceAll("%20", "-").replaceAll(" ", "-"); // remove path and spaces
//console.debug(DEBUG_PREFIX,"Current backgroung:",newBackground);
if (currentBackground !== newBackground) {
currentBackground = newBackground;
console.debug(DEBUG_PREFIX, "Changing ambient audio for", currentBackground);
updateAmbient();
}
}
//}
const context = getContext();
//console.debug(DEBUG_PREFIX,context);
if (context.chat.length == 0)
return;
let chatIsGroup = context.chat[0].is_group;
let newCharacter = null;
// 1) Update BGM (single chat)
// -----------------------------
if (!chatIsGroup) {
// Reset bgm list on new chat
if (context.chatId != current_chat_id) {
current_chat_id = context.chatId;
characterMusics = {};
cooldownBGM = 0;
}
newCharacter = context.name2;
//console.log(DEBUG_PREFIX,"SOLO CHAT MODE"); // DBG
// 1.1) First time loading chat
if (characterMusics[newCharacter] === undefined) {
await loadCharacterBGM(newCharacter);
currentExpressionBGM = FALLBACK_EXPRESSION;
//currentCharacterBGM = newCharacter;
//updateBGM();
//cooldownBGM = BGM_UPDATE_COOLDOWN;
return;
}
// 1.2) Switched chat
if (currentCharacterBGM !== newCharacter) {
currentCharacterBGM = newCharacter;
try {
await updateBGM(false, true);
cooldownBGM = extension_settings.audio.bgm_cooldown * 1000;
}
catch (error) {
console.debug(DEBUG_PREFIX, "Error while trying to update BGM character, will try again");
currentCharacterBGM = null
}
return;
}
const newExpression = getNewExpression();
// 1.3) Same character but different expression
if (currentExpressionBGM !== newExpression) {
// Check cooldown
if (cooldownBGM > 0) {
//console.debug(DEBUG_PREFIX,"(SOLO) BGM switch on cooldown:",cooldownBGM);
return;
}
try {
currentExpressionBGM = newExpression;
await updateBGM();
cooldownBGM = extension_settings.audio.bgm_cooldown * 1000;
console.debug(DEBUG_PREFIX, "(SOLO) Updated current character expression to", currentExpressionBGM, "cooldown", cooldownBGM);
}
catch (error) {
console.debug(DEBUG_PREFIX, "Error while trying to update BGM expression, will try again");
currentCharacterBGM = null
}
return;
}
return;
}
// 2) Update BGM (group chat)
// -----------------------------
// Load current chat character bgms
// Reset bgm list on new chat
if (context.chatId != current_chat_id) {
current_chat_id = context.chatId;
characterMusics = {};
cooldownBGM = 0;
for (const message of context.chat) {
if (characterMusics[message.name] === undefined)
await loadCharacterBGM(message.name);
}
try {
newCharacter = context.chat[context.chat.length - 1].name;
currentCharacterBGM = newCharacter;
await updateBGM(false, true);
cooldownBGM = extension_settings.audio.bgm_cooldown * 1000;
currentCharacterBGM = newCharacter;
currentExpressionBGM = FALLBACK_EXPRESSION;
console.debug(DEBUG_PREFIX, "(GROUP) Updated current character BGM to", currentExpressionBGM, "cooldown", cooldownBGM);
}
catch (error) {
console.debug(DEBUG_PREFIX, "Error while trying to update BGM group, will try again");
currentCharacterBGM = null
}
return;
}
newCharacter = context.chat[context.chat.length - 1].name;
const userName = context.name1;
if (newCharacter !== undefined && newCharacter != userName) {
//console.log(DEBUG_PREFIX,"GROUP CHAT MODE"); // DBG
// 2.1) New character appear
if (characterMusics[newCharacter] === undefined) {
await loadCharacterBGM(newCharacter);
return;
}
// 2.2) Switched char
if (currentCharacterBGM !== newCharacter) {
// Check cooldown
if (cooldownBGM > 0) {
console.debug(DEBUG_PREFIX, "(GROUP) BGM switch on cooldown:", cooldownBGM);
return;
}
try {
currentCharacterBGM = newCharacter;
await updateBGM();
cooldownBGM = extension_settings.audio.bgm_cooldown * 1000;
currentCharacterBGM = newCharacter;
currentExpressionBGM = FALLBACK_EXPRESSION;
console.debug(DEBUG_PREFIX, "(GROUP) Updated current character BGM to", currentExpressionBGM, "cooldown", cooldownBGM);
}
catch (error) {
console.debug(DEBUG_PREFIX, "Error while trying to update BGM group, will try again");
currentCharacterBGM = null
}
return;
}
/*
const newExpression = getNewExpression();
// 1.3) Same character but different expression
if (currentExpressionBGM !== newExpression) {
// Check cooldown
if (cooldownBGM > 0) {
console.debug(DEBUG_PREFIX,"BGM switch on cooldown:",cooldownBGM);
return;
}
cooldownBGM = BGM_UPDATE_COOLDOWN;
currentExpressionBGM = newExpression;
console.debug(DEBUG_PREFIX,"Updated current character expression to",currentExpressionBGM);
updateBGM();
return;
}
return;*/
}
// Case 3: Same character/expression or BGM switch on cooldown keep playing same BGM
//console.debug(DEBUG_PREFIX,"Nothing to do for",currentCharacterBGM, newCharacter, currentExpressionBGM, cooldownBGM);
}
}
async function loadCharacterBGM(newCharacter) {
console.debug(DEBUG_PREFIX, "New character detected, loading BGM folder of", newCharacter);
// 1.1) First time character appear, load its music folder
const audio_file_paths = await getCharacterBgmList(newCharacter);
//console.debug(DEBUG_PREFIX, "Recieved", audio_file_paths);
// Initialise expression/files mapping
characterMusics[newCharacter] = {};
for (const e of DEFAULT_EXPRESSIONS)
characterMusics[newCharacter][e] = [];
for (const i of audio_file_paths) {
//console.debug(DEBUG_PREFIX,"File found:",i);
for (const e of DEFAULT_EXPRESSIONS)
if (i.includes(e))
characterMusics[newCharacter][e].push(i);
}
console.debug(DEBUG_PREFIX, "Updated BGM map of", newCharacter, "to", characterMusics[newCharacter]);
fillBGMSelect();
}
function getNewExpression() {
let newExpression;
// HACK: use sprite file name as expression detection
if (!$(SPRITE_DOM_ID).length) {
console.error(DEBUG_PREFIX, "ERROR: expression sprite does not exist, cannot extract expression from ", SPRITE_DOM_ID)
return FALLBACK_EXPRESSION;
}
const spriteFile = $("#expression-image").attr("src");
newExpression = spriteFile.substring(spriteFile.lastIndexOf("/") + 1).replace(/\.[^/.]+$/, "");
//
// No sprite to detect expression
if (newExpression == "") {
//console.info(DEBUG_PREFIX,"Warning: no expression extracted from sprite, switch to",FALLBACK_EXPRESSION);
newExpression = FALLBACK_EXPRESSION;
}
if (!DEFAULT_EXPRESSIONS.includes(newExpression)) {
console.info(DEBUG_PREFIX, "Warning:", newExpression, " is not a handled expression, expected one of", FALLBACK_EXPRESSION);
return FALLBACK_EXPRESSION;
}
return newExpression;
}
async function updateBGM(isUserInput = false, newChat = false) {
if (!isUserInput && !extension_settings.audio.dynamic_bgm_enabled && $("#audio_bgm").attr("src") != "" && !bgmEnded && !newChat) {
console.debug(DEBUG_PREFIX, "BGM already playing and dynamic switch disabled, no update done");
return;
}
let audio_file_path = ""
if (isUserInput || (extension_settings.audio.bgm_locked && extension_settings.audio.bgm_selected !== null)) {
audio_file_path = extension_settings.audio.bgm_selected;
if (isUserInput)
console.debug(DEBUG_PREFIX, "User selected BGM", audio_file_path);
if (extension_settings.audio.bgm_locked)
console.debug(DEBUG_PREFIX, "BGM locked keeping current audio", audio_file_path);
}
else {
let audio_files = null;
if (extension_settings.audio.dynamic_bgm_enabled) {
extension_settings.audio.bgm_selected = null;
saveSettingsDebounced();
audio_files = characterMusics[currentCharacterBGM][currentExpressionBGM];// Try char expression BGM
if (audio_files === undefined || audio_files.length == 0) {
console.debug(DEBUG_PREFIX, "No BGM for", currentCharacterBGM, currentExpressionBGM);
audio_files = characterMusics[currentCharacterBGM][FALLBACK_EXPRESSION]; // Try char FALLBACK BGM
if (audio_files === undefined || audio_files.length == 0) {
console.debug(DEBUG_PREFIX, "No default BGM for", currentCharacterBGM, FALLBACK_EXPRESSION, "switch to ST BGM");
audio_files = fallback_BGMS; // ST FALLBACK BGM
if (audio_files.length == 0) {
console.debug(DEBUG_PREFIX, "No default BGM file found, bgm folder may be empty.");
return;
}
}
}
}
else {
audio_files = [];
$("#audio_bgm_select option").each(function () { audio_files.push($(this).val()); });
}
audio_file_path = audio_files[Math.floor(Math.random() * audio_files.length)];
}
console.log(DEBUG_PREFIX, "Updating BGM");
console.log(DEBUG_PREFIX, "Checking file", audio_file_path);
try {
const response = await fetch(audio_file_path);
if (!response.ok) {
console.log(DEBUG_PREFIX, "File not found!")
}
else {
console.log(DEBUG_PREFIX, "Switching BGM to", currentExpressionBGM);
$("#audio_bgm_select").val(audio_file_path);
const audio = $("#audio_bgm");
if (audio.attr("src") == audio_file_path && !bgmEnded) {
console.log(DEBUG_PREFIX, "Already playing, ignored");
return;
}
let fade_time = 2000;
bgmEnded = false;
if (isUserInput || extension_settings.audio.bgm_locked) {
audio.attr("src", audio_file_path);
audio[0].play();
}
else {
audio.animate({ volume: 0.0 }, fade_time, function () {
audio.attr("src", audio_file_path);
audio[0].play();
audio.volume = extension_settings.audio.bgm_volume * 0.01;
audio.animate({ volume: extension_settings.audio.bgm_volume * 0.01 }, fade_time);
});
}
}
} catch (error) {
console.log(DEBUG_PREFIX, "Error while trying to fetch", audio_file_path, ":", error);
}
}
async function updateAmbient(isUserInput = false) {
let audio_file_path = null;
if (isUserInput || extension_settings.audio.ambient_locked) {
audio_file_path = extension_settings.audio.ambient_selected;
if (isUserInput)
console.debug(DEBUG_PREFIX, "User selected Ambient", audio_file_path);
if (extension_settings.audio.bgm_locked)
console.debug(DEBUG_PREFIX, "Ambient locked keeping current audio", audio_file_path);
}
else {
extension_settings.audio.ambient_selected = null;
for (const i of ambients) {
console.debug(i)
if (i.includes(currentBackground)) {
audio_file_path = i;
break;
}
}
}
if (audio_file_path === null) {
console.debug(DEBUG_PREFIX, "No bgm file found for background", currentBackground);
const audio = $("#audio_ambient");
audio.attr("src", "");
audio[0].pause();
return;
}
//const audio_file_path = AMBIENT_FOLDER+currentBackground+".mp3";
console.log(DEBUG_PREFIX, "Updating ambient");
console.log(DEBUG_PREFIX, "Checking file", audio_file_path);
$("#audio_ambient_select").val(audio_file_path);
let fade_time = 2000;
if (isUserInput)
fade_time = 0;
const audio = $("#audio_ambient");
if (audio.attr("src") == audio_file_path) {
console.log(DEBUG_PREFIX, "Already playing, ignored");
return;
}
audio.animate({ volume: 0.0 }, fade_time, function () {
audio.attr("src", audio_file_path);
audio[0].play();
audio.volume = extension_settings.audio.ambient_volume * 0.01;
audio.animate({ volume: extension_settings.audio.ambient_volume * 0.01 }, fade_time);
});
}
/**
* Handles wheel events on volume sliders.
* @param {WheelEvent} e Event
*/
function onVolumeSliderWheelEvent(e) {
const slider = $(this);
e.preventDefault();
e.stopPropagation();
const delta = e.deltaY / 20;
const sliderVal = Number(slider.val());
let newVal = sliderVal - delta;
if (newVal < 0) {
newVal = 0;
} else if (newVal > 100) {
newVal = 100;
}
slider.val(newVal).trigger('input');
}
//#############################//
// Extension load //
//#############################//
// This function is called when the extension is loaded
jQuery(async () => {
const windowHtml = $(await $.get(`${extensionFolderPath}/window.html`));
$('#extensions_settings').append(windowHtml);
loadSettings();
$("#audio_enabled").on("click", onEnabledClick);
$("#audio_dynamic_bgm_enabled").on("click", onDynamicBGMEnabledClick);
//$("#audio_dynamic_ambient_enabled").on("click", onDynamicAmbientEnabledClick);
//$("#audio_bgm").attr("loop", false);
$("#audio_ambient").attr("loop", true);
$("#audio_bgm").hide();
$("#audio_bgm_lock").on("click", onBGMLockClick);
$("#audio_bgm_mute").on("click", onBGMMuteClick);
$("#audio_bgm_volume_slider").on("input", onBGMVolumeChange);
$("#audio_bgm_random").on("click", onBGMRandomClick);
$("#audio_ambient").hide();
$("#audio_ambient_lock").on("click", onAmbientLockClick);
$("#audio_ambient_mute").on("click", onAmbientMuteClick);
$("#audio_ambient_volume_slider").on("input", onAmbientVolumeChange);
document.getElementById('audio_ambient_volume_slider').addEventListener('wheel', onVolumeSliderWheelEvent, { passive: false });
document.getElementById('audio_bgm_volume_slider').addEventListener('wheel', onVolumeSliderWheelEvent, { passive: false });
$("#audio_bgm_cooldown").on("input", onBGMCooldownInput);
// Reset assets container, will be redected like if ST restarted
$("#audio_refresh_assets").on("click", function () {
console.debug(DEBUG_PREFIX, "Refreshing audio assets");
current_chat_id = null
fallback_BGMS = null;
ambients = null;
characterMusics = {};
currentCharacterBGM = null;
currentExpressionBGM = null;
currentBackground = null;
})
$("#audio_bgm_select").on("change", onBGMSelectChange);
$("#audio_ambient_select").on("change", onAmbientSelectChange);
// DBG
$("#audio_debug").on("click", function () {
if ($("#audio_debug").is(':checked')) {
$("#audio_bgm").show();
$("#audio_ambient").show();
}
else {
$("#audio_bgm").hide();
$("#audio_ambient").hide();
}
});
//
$("#audio_bgm").on("ended", function () {
console.debug(DEBUG_PREFIX, "END OF BGM")
if (!extension_settings.audio.bgm_locked) {
bgmEnded = true;
updateBGM();
}
});
const wrapper = new ModuleWorkerWrapper(moduleWorker);
setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL);
moduleWorker();
});

View File

@@ -1,11 +0,0 @@
{
"display_name": "Dynamic Audio",
"loading_order": 14,
"requires": [],
"optional": ["classify"],
"js": "index.js",
"css": "style.css",
"author": "Keij#6799 and Deffcolony",
"version": "0.1.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
}

View File

@@ -1,92 +0,0 @@
.audio-ui-block {
margin-bottom: 1em;
}
.audio-mixer-div {
display: flex;
flex-direction: row;
padding: 5px;
background-color: rgba(38, 38, 38, 0.5);
border: 1px rgb(75, 75, 75) solid;
border-radius: 10px;
}
.audio-label {
display: block;
text-align: center;
}
.audio-volume-div {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.audio-lock-button {
width: 100%;
height: 2em;
}
.audio-random-button {
width: 100%;
height: 2em;
}
.audio-mute-button {
width: 100%;
height: 2em;
}
.audio-slider {
width: 100% !important;
vertical-align: center;
}
.audio-mute-button-muted {
color: red;
}
#audio_refresh_assets {
width: 50px;
height: 30px;
}
.audio-mixer-mute {
width: 10%;
}
.audio-mixer-lock {
width: 10%;
}
.audio-mixer-random {
width: 10%;
}
.audio-container {
display: flex;
gap: 10px;
align-items: center;
}
.audio-container>.vol {
width: 100px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.audio-container>.vol>input {
width: 100%;
}
.audio-container>.playlist {
flex-grow: 1;
}
.audio-container>.playlist>select {
height: 100%;
margin: 0 !important;
}

View File

@@ -1,103 +0,0 @@
<div id="audio_settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>Dynamic Audio</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<div>
<label class="checkbox_label" for="audio_enabled">
<input type="checkbox" id="audio_enabled" name="audio_enabled">
<small>Enabled</small>
</label>
<div id="audio_bgm_dynamic_enable_div">
<label class="checkbox_label" for="audio_dynamic_bgm_enabled">
<input type="checkbox" id="audio_dynamic_bgm_enabled" name="audio_dynamic_bgm_enabled">
<small>Enable expression BGM switch (req. character expression)</small>
</label>
</div>
<div id="audio_debug_div">
<label class="checkbox_label" for="audio_debug">
<input type="checkbox" id="audio_debug" name="audio_debug">
<small>Debug</small>
</label>
</div>
<div>
<label for="audio_refresh_assets">Refresh assets</label>
<div id="audio_refresh_assets" class="menu_button">
<i class="fa-solid fa-refresh fa-lg"></i>
</div>
</div>
</div>
<div>
<div class="audio-ui-block">
<label for="audio_bgm_volume_slider">Music</label>
<div class="audio-mixer-div audio-container">
<div class="audio-mixer-element audio-mixer-mute">
<div id="audio_bgm_mute" class="menu_button audio-mute-button">
<i class="fa-solid fa-volume-high fa-lg fa-fw" id="audio_bgm_mute_icon"></i>
</div>
</div>
<div class="audio-mixer-element vol audio-mixer-volume">
<input type="range" class ="audio-slider" id ="audio_bgm_volume_slider" value = "0" maxlength ="100">
</div>
<div class="audio-mixer-element playlist audio-mixer-playlist">
<select id="audio_bgm_select">
</select>
</div>
<div class="audio-mixer-element audio-mixer-lock">
<div id="audio_bgm_lock" class="menu_button audio-lock-button">
<i class="fa-solid fa-repeat fa-lg fa-fw" id="audio_bgm_lock_icon"></i>
</div>
</div>
<div class="audio-mixer-element audio-mixer-random">
<div id="audio_bgm_random" class="menu_button audio-random-button">
<i class="fa-solid fa-random fa-lg fa-fw" id="audio_bgm_random_icon"></i>
</div>
</div>
</div>
<audio id="audio_bgm" controls src="">
</div>
<div>
<label for="audio_ambient_volume_slider">Ambient</label>
<div class="audio-mixer-div audio-container">
<div class="audio-mixer-element audio-mixer-mute">
<div id="audio_ambient_mute" class="menu_button audio-mute-button">
<i class="fa-solid fa-volume-high fa-lg fa-fw" id="audio_ambient_mute_icon"></i>
</div>
</div>
<div class="audio-mixer-element vol audio-mixer-volume">
<input type="range" class ="audio-slider" id ="audio_ambient_volume_slider" value = "0" maxlength ="100">
</div>
<div class="audio-mixer-element playlist audio-mixer-playlist">
<select id="audio_ambient_select">
</select>
</div>
<div class="audio-mixer-element">
<div id="audio_ambient_lock" class="menu_button audio-lock-button">
<i class="fa-solid fa-lock-open fa-lg fa-fw" id="audio_ambient_lock_icon"></i>
</div>
</div>
</div>
<audio id="audio_ambient" controls src="">
</div>
<div>
<label for="audio_bgm_cooldown">Music update cooldown (in seconds)</label>
<input id="audio_bgm_cooldown" class="text_pole wide30p">
</div>
</div>
<div>
<b>Hint:</b>
<i>
Create new folder in the
<b>public/characters/</b>
folder and name it as the name of the character.
Create a folder name <b>bgm</b> inside of it.
Put bgm music with expressions there. File names should follow the pattern:
<it>[expression_label]_[number].mp3</it>
By default one of the <it>neutral_[number].mp3</it> will play if classify module is not active.
</i>
</div>
</div>
</div>
</div>

View File

@@ -1,178 +0,0 @@
import { eventSource, event_types, generateQuietPrompt } from "../../../script.js";
import { getContext, saveMetadataDebounced } from "../../extensions.js";
import { registerSlashCommand } from "../../slash-commands.js";
import { stringFormat } from "../../utils.js";
export { MODULE_NAME };
const MODULE_NAME = 'backgrounds';
const METADATA_KEY = 'custom_background';
/**
* @param {string} background
*/
function forceSetBackground(background) {
saveBackgroundMetadata(background);
setCustomBackground();
}
async function moduleWorker() {
if (hasCustomBackground()) {
$('#unlock_background').show();
$('#lock_background').hide();
setCustomBackground();
}
else {
$('#unlock_background').hide();
$('#lock_background').show();
unsetCustomBackground();
}
}
function onLockBackgroundClick() {
const bgImage = window.getComputedStyle(document.getElementById('bg1')).backgroundImage;
// Extract the URL from the CSS string
const urlRegex = /url\((['"])?(.*?)\1\)/;
const matches = bgImage.match(urlRegex);
const url = matches[2];
// Remove the protocol and host, leaving the relative URL
const relativeUrl = new URL(url).pathname;
const relativeBgImage = `url("${relativeUrl}")`
saveBackgroundMetadata(relativeBgImage);
setCustomBackground();
$('#unlock_background').show();
$('#lock_background').hide();
}
function onUnlockBackgroundClick() {
removeBackgroundMetadata();
unsetCustomBackground();
$('#unlock_background').hide();
$('#lock_background').show();
}
function hasCustomBackground() {
const context = getContext();
return !!context.chatMetadata[METADATA_KEY];
}
function saveBackgroundMetadata(file) {
const context = getContext();
context.chatMetadata[METADATA_KEY] = file;
saveMetadataDebounced();
}
function removeBackgroundMetadata() {
const context = getContext();
delete context.chatMetadata[METADATA_KEY];
saveMetadataDebounced();
}
function setCustomBackground() {
const context = getContext();
const file = context.chatMetadata[METADATA_KEY];
// bg already set
if (document.getElementById("bg_custom").style.backgroundImage == file) {
return;
}
$("#bg_custom").css("background-image", file);
$("#custom_bg_preview").css("background-image", file);
}
function unsetCustomBackground() {
$("#bg_custom").css("background-image", 'none');
$("#custom_bg_preview").css("background-image", 'none');
}
function onSelectBackgroundClick() {
const bgfile = $(this).attr("bgfile");
if (hasCustomBackground()) {
saveBackgroundMetadata(`url("backgrounds/${bgfile}")`);
setCustomBackground();
}
}
const autoBgPrompt = `Pause your roleplay and choose a location ONLY from the provided list that is the most suitable for the current scene. Do not output any other text:\n{0}`;
async function autoBackgroundCommand() {
const options = Array.from(document.querySelectorAll('.BGSampleTitle')).map(x => ({ element: x, text: x.innerText.trim() })).filter(x => x.text.length > 0);
if (options.length == 0) {
toastr.warning('No backgrounds to choose from. Please upload some images to the "backgrounds" folder.');
return;
}
const list = options.map(option => `- ${option.text}`).join('\n');
const prompt = stringFormat(autoBgPrompt, list);
const reply = await generateQuietPrompt(prompt);
const fuse = new Fuse(options, { keys: ['text'] });
const bestMatch = fuse.search(reply, { limit: 1 });
if (bestMatch.length == 0) {
toastr.warning('No match found. Please try again.');
return;
}
console.debug('Automatically choosing background:', bestMatch);
bestMatch[0].item.element.click();
}
$(document).ready(function () {
function addSettings() {
const html = `
<div class="background_settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>Chat Backgrounds</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<div class="background_controls">
<div id="lock_background" class="menu_button">
<i class="fa-solid fa-lock"></i>
Lock
</div>
<div id="unlock_background" class="menu_button">
<i class="fa-solid fa-unlock"></i>
Unlock
</div>
<small>
Press "Lock" to assign a currently selected background to a character or group chat.<br>
Any background image selected while lock is engaged will be saved automatically.
</small>
</div>
<div class="background_controls">
<div id="auto_background" class="menu_button">
<i class="fa-solid fa-wand-magic"></i>
Auto
</div>
<small>
Automatically select a background based on the chat context.<br>
Respects the "Lock" setting state.
</small>
</div>
<div>Preview</div>
<div id="custom_bg_preview">
</div>
</div>
</div>
</div>
`;
$('#extensions_settings').append(html);
$('#lock_background').on('click', onLockBackgroundClick);
$('#unlock_background').on('click', onUnlockBackgroundClick);
$(document).on("click", ".bg_example", onSelectBackgroundClick);
$('#auto_background').on("click", autoBackgroundCommand);
}
addSettings();
registerSlashCommand('lockbg', onLockBackgroundClick, ['bglock'], " locks a background for the currently selected chat", true, true);
registerSlashCommand('unlockbg', onUnlockBackgroundClick, ['bgunlock'], ' unlocks a background for the currently selected chat', true, true);
registerSlashCommand('autobg', autoBackgroundCommand, ['bgauto'], ' automatically changes the background based on the chat context using the AI request prompt', true, true);
eventSource.on(event_types.FORCE_SET_BACKGROUND, forceSetBackground);
eventSource.on(event_types.CHAT_CHANGED, moduleWorker);
});

View File

@@ -1,11 +0,0 @@
{
"display_name": "Chat Backgrounds",
"loading_order": 7,
"requires": [],
"optional": [],
"js": "index.js",
"css": "style.css",
"author": "Cohee#1207",
"version": "1.0.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
}

View File

@@ -1,45 +0,0 @@
#custom_bg_preview {
width: 160px;
height: 90px;
background-color: var(--grey30a);
background-repeat: no-repeat;
background-attachment: fixed;
background-size: cover;
border-radius: 20px;
border: 1px solid var(--SmartThemeBorderColor);
box-shadow: 0 0 7px var(--black50a);
margin: 5px;
}
#custom_bg_preview::before {
content: 'No Background';
color: white;
position: relative;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
#custom_bg_preview:not([style*="background-image: none"])::before {
display: none;
}
.background_controls .menu_button {
display: flex;
flex-direction: row;
align-items: center;
column-gap: 10px;
}
.background_controls {
display: flex;
flex-direction: row;
align-items: center;
column-gap: 10px;
}
.background_controls small {
flex-grow: 1;
}

View File

@@ -1,11 +0,0 @@
{
"display_name": "Bulk Card Editor",
"loading_order": 9,
"requires": [],
"optional": [],
"js": "index.js",
"css": "style.css",
"author": "city-unit",
"version": "1.0.0",
"homePage": "https://github.com/city-unit"
}

View File

@@ -1,7 +0,0 @@
.bulk_select_checkbox {
align-self: center;
}
#rm_print_characters_block.bulk_select .wide100pLess70px {
width: calc(100% - 85px);
}

View File

@@ -1,17 +1,42 @@
import { getBase64Async, saveBase64AsFile } from "../../utils.js";
import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules } from "../../extensions.js";
import { callPopup, getRequestHeaders, saveSettingsDebounced } from "../../../script.js";
import { appendImageToMessage, callPopup, getRequestHeaders, saveSettingsDebounced, substituteParams } from "../../../script.js";
import { getMessageTimeStamp } from "../../RossAscends-mods.js";
import { SECRET_KEYS, secret_state } from "../../secrets.js";
import { isImageInliningSupported } from "../../openai.js";
export { MODULE_NAME };
const MODULE_NAME = 'caption';
const UPDATE_INTERVAL = 1000;
const PROMPT_DEFAULT = 'Whats in this image?';
const TEMPLATE_DEFAULT = '[{{user}} sends {{char}} a picture that contains: {{caption}}]';
async function moduleWorker() {
const hasConnection = getContext().onlineStatus !== 'no_connection';
$('#send_picture').toggle(hasConnection);
}
function migrateSettings() {
if (extension_settings.caption.local !== undefined) {
extension_settings.caption.source = extension_settings.caption.local ? 'local' : 'extras';
}
delete extension_settings.caption.local;
if (!extension_settings.caption.source) {
extension_settings.caption.source = 'extras';
}
if (!extension_settings.caption.prompt) {
extension_settings.caption.prompt = PROMPT_DEFAULT;
}
if (!extension_settings.caption.template) {
extension_settings.caption.template = TEMPLATE_DEFAULT;
}
}
async function setImageIcon() {
try {
const sendButton = $('#send_picture .extensionsMenuExtensionButton');
@@ -36,7 +61,14 @@ async function setSpinnerIcon() {
async function sendCaptionedMessage(caption, image) {
const context = getContext();
let messageText = `[${context.name1} sends ${context.name2 ?? ''} a picture that contains: ${caption}]`;
let template = extension_settings.caption.template || TEMPLATE_DEFAULT;
if (!/{{caption}}/i.test(template)) {
console.warn('Poka-yoke: Caption template does not contain {{caption}}. Appending it.')
template += ' {{caption}}';
}
let messageText = substituteParams(template).replace(/{{caption}}/i, caption);
if (extension_settings.caption.refine_mode) {
messageText = await callPopup(
@@ -65,42 +97,96 @@ async function sendCaptionedMessage(caption, image) {
await context.generate('caption');
}
async function doCaptionRequest(base64Img) {
if (extension_settings.caption.local) {
const apiResult = await fetch('/api/extra/caption', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ image: base64Img })
});
/**
*
* @param {string} base64Img Base64 encoded image without the data:image/...;base64, prefix
* @param {string} fileData Base64 encoded image with the data:image/...;base64, prefix
* @returns
*/
async function doCaptionRequest(base64Img, fileData) {
switch (extension_settings.caption.source) {
case 'local':
return await captionLocal(base64Img);
case 'extras':
return await captionExtras(base64Img);
case 'horde':
return await captionHorde(base64Img);
case 'openai':
return await captionOpenAI(fileData);
default:
throw new Error('Unknown caption source.');
}
}
if (!apiResult.ok) {
throw new Error('Failed to caption image via local pipeline.');
}
const data = await apiResult.json();
return data;
} else if (modules.includes('caption')) {
const url = new URL(getApiUrl());
url.pathname = '/api/caption';
const apiResult = await doExtrasFetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Bypass-Tunnel-Reminder': 'bypass',
},
body: JSON.stringify({ image: base64Img })
});
if (!apiResult.ok) {
throw new Error('Failed to caption image via Extras.');
}
const data = await apiResult.json();
return data;
} else {
async function captionExtras(base64Img) {
if (!modules.includes('caption')) {
throw new Error('No captioning module is available.');
}
const url = new URL(getApiUrl());
url.pathname = '/api/caption';
const apiResult = await doExtrasFetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Bypass-Tunnel-Reminder': 'bypass',
},
body: JSON.stringify({ image: base64Img })
});
if (!apiResult.ok) {
throw new Error('Failed to caption image via Extras.');
}
const data = await apiResult.json();
return data;
}
async function captionLocal(base64Img) {
const apiResult = await fetch('/api/extra/caption', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ image: base64Img })
});
if (!apiResult.ok) {
throw new Error('Failed to caption image via local pipeline.');
}
const data = await apiResult.json();
return data;
}
async function captionHorde(base64Img) {
const apiResult = await fetch('/api/horde/caption-image', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ image: base64Img })
});
if (!apiResult.ok) {
throw new Error('Failed to caption image via Horde.');
}
const data = await apiResult.json();
return data;
}
async function captionOpenAI(base64Img) {
const prompt = extension_settings.caption.prompt || PROMPT_DEFAULT;
const apiResult = await fetch('/api/openai/caption-image', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ image: base64Img, prompt: prompt }),
});
if (!apiResult.ok) {
throw new Error('Failed to caption image via OpenAI.');
}
const data = await apiResult.json();
return data;
}
async function onSelectImage(e) {
@@ -116,7 +202,7 @@ async function onSelectImage(e) {
const fileData = await getBase64Async(file);
const base64Format = fileData.split(',')[0].split(';')[0].split('/')[1];
const base64Data = fileData.split(',')[1];
const data = await doCaptionRequest(base64Data);
const data = await doCaptionRequest(base64Data, fileData);
const caption = data.caption;
const imageToSave = data.thumbnail ? data.thumbnail : base64Data;
const format = data.thumbnail ? 'jpeg' : base64Format;
@@ -138,6 +224,83 @@ function onRefineModeInput() {
saveSettingsDebounced();
}
async function sendEmbeddedImage(e) {
const file = e.target.files[0];
if (!file || !(file instanceof File)) {
return;
}
try {
const context = getContext();
const fileData = await getBase64Async(file);
const base64Format = fileData.split(',')[0].split(';')[0].split('/')[1];
const base64Data = fileData.split(',')[1];
const caption = await callPopup('<h3>Enter a comment or question (optional)</h3>', 'input', 'What is this?', { okButton: 'Send', rows: 2 });
const imagePath = await saveBase64AsFile(base64Data, context.name2, '', base64Format);
const message = {
name: context.name1,
is_user: true,
send_date: getMessageTimeStamp(),
mes: caption || `[${context.name1} sends ${context.name2} a picture]`,
extra: {
image: imagePath,
inline_image: !!caption,
title: caption || '',
},
};
context.chat.push(message);
context.addOneMessage(message);
await context.generate('caption');
}
catch (error) {
console.log(error);
}
finally {
e.target.form.reset();
setImageIcon();
}
}
function onImageEmbedClicked() {
const context = getContext();
const messageElement = $(this).closest('.mes');
const messageId = messageElement.attr('mesid');
const message = context.chat[messageId];
if (!message) {
console.warn('Failed to find message with id', messageId);
return;
}
$('#embed_img_file')
.off('change')
.on('change', parseAndUploadEmbed)
.trigger('click');
async function parseAndUploadEmbed(e) {
const file = e.target.files[0];
if (!file || !(file instanceof File)) {
return;
}
const fileData = await getBase64Async(file);
const base64Data = fileData.split(',')[1];
const base64Format = fileData.split(',')[0].split(';')[0].split('/')[1];
const imagePath = await saveBase64AsFile(base64Data, context.name2, '', base64Format);
if (!message.extra) {
message.extra = {};
}
message.extra.image = imagePath;
message.extra.inline_image = true;
message.extra.title = '';
appendImageToMessage(message, messageElement);
await context.saveChat();
}
}
jQuery(function () {
function addSendPictureButton() {
const sendButton = $(`
@@ -149,10 +312,20 @@ jQuery(function () {
$('#extensionsMenu').prepend(sendButton);
$(sendButton).hide();
$(sendButton).on('click', () => {
const hasCaptionModule = modules.includes('caption') || extension_settings.caption.local;
if (isImageInliningSupported()) {
console.log('Native image inlining is supported. Skipping captioning.');
$('#embed_img_file').off('change').on('change', sendEmbeddedImage).trigger('click');
return;
}
const hasCaptionModule =
(modules.includes('caption') && extension_settings.caption.source === 'extras') ||
(extension_settings.caption.source === 'openai' && secret_state[SECRET_KEYS.OPENAI]) ||
extension_settings.caption.source === 'local' ||
extension_settings.caption.source === 'horde';
if (!hasCaptionModule) {
toastr.error('No captioning module is available. Either enable the local captioning pipeline or connect to Extras.');
toastr.error('No captioning module is available. Choose other captioning source in the extension settings.');
return;
}
@@ -160,10 +333,12 @@ jQuery(function () {
});
}
function addPictureSendForm() {
const inputHtml = `<input id="img_file" type="file" accept="image/*">`;
const inputHtml = `<input id="img_file" type="file" hidden accept="image/*">`;
const embedInputHtml = `<input id="embed_img_file" type="file" hidden accept="image/*">`;
const imgForm = document.createElement('form');
imgForm.id = 'img_form';
$(imgForm).append(inputHtml);
$(imgForm).append(embedInputHtml);
$(imgForm).hide();
$('#form_sheld').append(imgForm);
$('#img_file').on('change', onSelectImage);
@@ -177,11 +352,18 @@ jQuery(function () {
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<label class="checkbox_label" for="caption_local">
<input id="caption_local" type="checkbox" class="checkbox">
Use local captioning pipeline
</label>
<label class="checkbox_label" for="caption_refine_mode">
<label for="caption_source">Source:</label>
<select id="caption_source" class="text_pole">
<option value="local">Local</option>
<option value="extras">Extras</option>
<option value="horde">Horde</option>
<option value="openai">OpenAI</option>
</select>
<label for="caption_prompt">Caption Prompt (OpenAI):</label>
<textarea id="caption_prompt" class="text_pole" rows="1" placeholder="&lt; Use default &gt;">${PROMPT_DEFAULT}</textarea>
<label for="caption_template">Message Template: <small>(use <tt>{{caption}}</tt> macro)</small></label>
<textarea id="caption_template" class="text_pole" rows="2" placeholder="&lt; Use default &gt;">${TEMPLATE_DEFAULT}</textarea>
<label class="checkbox_label margin-bot-10px" for="caption_refine_mode">
<input id="caption_refine_mode" type="checkbox" class="checkbox">
Edit captions before generation
</label>
@@ -196,13 +378,26 @@ jQuery(function () {
addPictureSendForm();
addSendPictureButton();
setImageIcon();
migrateSettings();
moduleWorker();
$('#caption_refine_mode').prop('checked', !!(extension_settings.caption.refine_mode));
$('#caption_local').prop('checked', !!(extension_settings.caption.local));
$('#caption_source').val(extension_settings.caption.source);
$('#caption_prompt').val(extension_settings.caption.prompt);
$('#caption_template').val(extension_settings.caption.template);
$('#caption_refine_mode').on('input', onRefineModeInput);
$('#caption_local').on('input', () => {
extension_settings.caption.local = !!$('#caption_local').prop('checked');
$('#caption_source').on('change', () => {
extension_settings.caption.source = String($('#caption_source').val());
saveSettingsDebounced();
});
$('#caption_prompt').on('input', () => {
extension_settings.caption.prompt = String($('#caption_prompt').val());
saveSettingsDebounced();
});
$('#caption_template').on('input', () => {
extension_settings.caption.template = String($('#caption_template').val());
saveSettingsDebounced();
});
$(document).on('click', '.mes_embed', onImageEmbedClicked);
setInterval(moduleWorker, UPDATE_INTERVAL);
});

View File

@@ -1,11 +0,0 @@
{
"display_name": "CFG",
"loading_order": 1,
"requires": [],
"optional": [],
"js": "index.js",
"css": "style.css",
"author": "kingbri",
"version": "1.0.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
}

View File

@@ -1,4 +0,0 @@
<a id="option_toggle_CFG">
<i class="fa-lg fa-solid fa-scale-balanced"></i>
<span data-i18n="CFG Scale">CFG Scale</span>
</a>

View File

@@ -1,90 +0,0 @@
import { chat_metadata, substituteParams, this_chid } from "../../../script.js";
import { extension_settings, getContext } from "../../extensions.js"
import { selected_group } from "../../group-chats.js";
import { getCharaFilename } from "../../utils.js";
export const cfgType = {
chat: 0,
chara: 1,
global: 2
}
export const metadataKeys = {
guidance_scale: "cfg_guidance_scale",
negative_prompt: "cfg_negative_prompt",
positive_prompt: "cfg_positive_prompt",
prompt_combine: "cfg_prompt_combine",
groupchat_individual_chars: "cfg_groupchat_individual_chars",
prompt_insertion_depth: "cfg_prompt_insertion_depth",
prompt_separator: "cfg_prompt_separator"
}
// Gets the CFG guidance scale
// If the guidance scale is 1, ignore the CFG prompt(s) since it won't be used anyways
export function getGuidanceScale() {
const charaCfg = extension_settings.cfg.chara?.find((e) => e.name === getCharaFilename(this_chid));
const chatGuidanceScale = chat_metadata[metadataKeys.guidance_scale];
const groupchatCharOverride = chat_metadata[metadataKeys.groupchat_individual_chars] ?? false;
if (chatGuidanceScale && chatGuidanceScale !== 1 && !groupchatCharOverride) {
return {
type: cfgType.chat,
value: chatGuidanceScale
};
}
if ((!selected_group && charaCfg || groupchatCharOverride) && charaCfg?.guidance_scale !== 1) {
return {
type: cfgType.chara,
value: charaCfg.guidance_scale
};
}
if (extension_settings.cfg.global && extension_settings.cfg.global?.guidance_scale !== 1) {
return {
type: cfgType.global,
value: extension_settings.cfg.global.guidance_scale
};
}
}
// Gets the CFG prompt
export function getCfgPrompt(guidanceScale, isNegative) {
let splitCfgPrompt = [];
const cfgPromptCombine = chat_metadata[metadataKeys.prompt_combine] ?? [];
if (guidanceScale.type === cfgType.chat || cfgPromptCombine.includes(cfgType.chat)) {
splitCfgPrompt.unshift(
substituteParams(
chat_metadata[isNegative ? metadataKeys.negative_prompt : metadataKeys.positive_prompt]
)
);
}
const charaCfg = extension_settings.cfg.chara?.find((e) => e.name === getCharaFilename(this_chid));
if (guidanceScale.type === cfgType.chara || cfgPromptCombine.includes(cfgType.chara)) {
splitCfgPrompt.unshift(
substituteParams(
isNegative ? charaCfg.negative_prompt : charaCfg.positive_prompt
)
);
}
if (guidanceScale.type === cfgType.global || cfgPromptCombine.includes(cfgType.global)) {
splitCfgPrompt.unshift(
substituteParams(
isNegative ? extension_settings.cfg.global.negative_prompt : extension_settings.cfg.global.positive_prompt
)
);
}
// This line is a bit hacky with a JSON.stringify and JSON.parse. Fix this if possible.
const customSeparator = JSON.parse(chat_metadata[metadataKeys.prompt_separator] || JSON.stringify("\n")) ?? "\n";
const combinedCfgPrompt = splitCfgPrompt.filter((e) => e.length > 0).join(customSeparator);
const insertionDepth = chat_metadata[metadataKeys.prompt_insertion_depth] ?? 1;
console.log(`Setting CFG with guidance scale: ${guidanceScale.value}, negatives: ${combinedCfgPrompt}`);
return {
value: combinedCfgPrompt,
depth: insertionDepth
};
}

View File

@@ -1,172 +0,0 @@
<div id="cfgConfig" class="drawer-content flexGap5">
<div class="panelControlBar flex-container">
<div id="cfgConfigHeader" class="fa-solid fa-grip drag-grabber"></div>
<div id="CFGClose" class="fa-solid fa-circle-xmark"></div>
</div>
<div name="cfgConfigHolder" class="scrollY">
<div id="chat_cfg_container">
<div class="inline-drawer">
<div id="CFGBlockToggle" class="inline-drawer-toggle inline-drawer-header">
<b>Chat CFG</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<small>
<b>Unique to this chat.</b><br>
</small>
<label for="chat_cfg_guidance_scale">
<span data-i18n="Scale">Scale</span>
<small data-i18n="1 = disabled">1 = disabled</small>
</label>
<div class="range-block-range-and-counter">
<div class="range-block-range">
<input type="range" id="chat_cfg_guidance_scale" name="volume" min="0.10" max="4.00" step="0.05">
</div>
<div class="range-block-counter">
<div contenteditable="true" data-for="chat_cfg_guidance_scale" id="chat_cfg_guidance_scale_counter">
select
</div>
</div>
</div>
<div>
<label for="chat_cfg_negative_prompt">
<span data-i18n="Negative Prompt">Negative Prompt</span>
</label>
<textarea id="chat_cfg_negative_prompt" rows="2" class="text_pole textarea_compact" data-i18n="[placeholder]write short replies, write replies using past tense" placeholder="write short replies, write replies using past tense"></textarea>
<label for="chat_cfg_positive_prompt">
<span data-i18n="Positive Prompt">Positive Prompt</span>
</label>
<textarea id="chat_cfg_positive_prompt" rows="2" class="text_pole textarea_compact" data-i18n="[placeholder]write short replies, write replies using past tense" placeholder="write short replies, write replies using past tense"></textarea>
</div>
<div id="groupchat_cfg_use_chara_container">
<label class="checkbox_label" for="groupchat_cfg_use_chara">
<input type="checkbox" id="groupchat_cfg_use_chara" />
<span data-i18n="Use character CFG scales">Use character CFG scales</span>
</label>
</div>
</div>
</div>
</div>
<div id="chara_cfg_container" style="display: none;">
<hr class="sysHR">
<div class="inline-drawer">
<div id="charaANBlockToggle" class="inline-drawer-toggle inline-drawer-header">
<b>Character CFG</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<small><b>Will be automatically added as the CFG for this character.</b></small>
<br />
<label for="chara_cfg_guidance_scale">
<span data-i18n="Scale">Scale</span>
<small data-i18n="1 = disabled">1 = disabled</small>
</label>
<div class="range-block-range-and-counter">
<div class="range-block-range">
<input type="range" id="chara_cfg_guidance_scale" name="volume" min="0.10" max="4.00" step="0.05">
</div>
<div class="range-block-counter">
<div contenteditable="true" data-for="chara_cfg_guidance_scale" id="chara_cfg_guidance_scale_counter">
select
</div>
</div>
</div>
<div>
<label for="chara_cfg_negative_prompt">
<span data-i18n="Negative Prompt">Negative Prompt</span>
</label>
<textarea id="chara_cfg_negative_prompt" rows="2" class="text_pole textarea_compact" data-i18n="[placeholder]write short replies, write replies using past tense" placeholder="write short replies, write replies using past tense"></textarea>
<label for="chara_cfg_positive_prompt">
<span data-i18n="Positive Prompt">Positive Prompt</span>
</label>
<textarea id="chara_cfg_positive_prompt" rows="2" class="text_pole textarea_compact" data-i18n="[placeholder]write short replies, write replies using past tense" placeholder="write short replies, write replies using past tense"></textarea>
</div>
</div>
</div>
</div>
<div id="global_cfg_container">
<hr class="sysHR">
<div class="inline-drawer">
<div id="defaultANBlockToggle" class="inline-drawer-toggle inline-drawer-header">
<b>Global CFG</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<small><b>Will be used as the default CFG options for every chat unless overridden.</b></small>
<br />
<label for="global_cfg_guidance_scale">
<span data-i18n="Scale">Scale</span>
<small data-i18n="1 = disabled">1 = disabled</small>
</label>
<div class="range-block-range-and-counter">
<div class="range-block-range">
<input type="range" id="global_cfg_guidance_scale" name="volume" min="0.10" max="4.00" step="0.05">
</div>
<div class="range-block-counter">
<div contenteditable="true" data-for="global_cfg_guidance_scale" id="global_cfg_guidance_scale_counter">
select
</div>
</div>
</div>
<div>
<label for="global_cfg_negative_prompt">
<span data-i18n="Negative Prompt">Negative Prompt</span>
</label>
<textarea id="global_cfg_negative_prompt" rows="2" class="text_pole textarea_compact" data-i18n="[placeholder]write short replies, write replies using past tense" placeholder="write short replies, write replies using past tense"></textarea>
<label for="global_cfg_positive_prompt">
<span data-i18n="Positive Prompt">Positive Prompt</span>
</label>
<textarea id="global_cfg_positive_prompt" rows="2" class="text_pole textarea_compact" data-i18n="[placeholder]write short replies, write replies using past tense" placeholder="write short replies, write replies using past tense"></textarea>
</div>
</div>
</div>
</div>
<div id="cfg_prompt_combine_container">
<hr class="sysHR">
<div class="inline-drawer">
<div id="defaultANBlockToggle" class="inline-drawer-toggle inline-drawer-header">
<b>CFG Prompt Cascading</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<div class="flex-container flexFlowColumn">
<small>
<b>Combine positive/negative prompts from other boxes.</b>
<br />
For example, ticking the chat, global, and character boxes combine all negative prompts into a comma-separated string.
</small>
</div>
<br />
<div class="flex-container flexFlowColumn">
<label for="cfg_prompt_combine">
<span data-i18n="Scale">Always Include</span>
</label>
<label class="checkbox_label">
<input type="checkbox" name="cfg_prompt_combine" value="0" />
<span data-i18n="Chat Negatives">Chat Negatives</span>
</label>
<label class="checkbox_label">
<input type="checkbox" name="cfg_prompt_combine" value="1" />
<span data-i18n="Character Negatives">Character Negatives</span>
</label>
<label class="checkbox_label">
<input type="checkbox" name="cfg_prompt_combine" value="2" />
<span data-i18n="Global Negatives">Global Negatives</span>
</label>
</div>
<div class="flex-container flexFlowColumn">
<label>
Custom Separator: <input id="cfg_prompt_separator" class="text_pole textarea_compact widthUnset" placeholder="&quot;\n&quot;" type="text" />
</label>
<label>
Insertion Depth: <input id="cfg_prompt_insertion_depth" class="text_pole widthUnset" type="number" min="0" max="99" />
</label>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,88 +0,0 @@
import { callPopup } from "../../../script.js";
import { getContext } from "../../extensions.js";
import { registerSlashCommand } from "../../slash-commands.js";
export { MODULE_NAME };
const MODULE_NAME = 'dice';
const UPDATE_INTERVAL = 1000;
async function doDiceRoll(customDiceFormula) {
let value = typeof customDiceFormula === 'string' ? customDiceFormula.trim() : $(this).data('value');
if (value == 'custom') {
value = await callPopup('Enter the dice formula:<br><i>(for example, <tt>2d6</tt>)</i>', 'input');
}
if (!value) {
return;
}
const isValid = droll.validate(value);
if (isValid) {
const result = droll.roll(value);
const context = getContext();
context.sendSystemMessage('generic', `${context.name1} rolls a ${value}. The result is: ${result.total} (${result.rolls})`, { isSmallSys: true });
} else {
toastr.warning('Invalid dice formula');
}
}
function addDiceRollButton() {
const buttonHtml = `
<div id="roll_dice" class="list-group-item flex-container flexGap5">
<div class="fa-solid fa-dice extensionsMenuExtensionButton" title="Roll Dice" /></div>
Roll Dice
</div>
`;
const dropdownHtml = `
<div id="dice_dropdown">
<ul class="list-group">
<li class="list-group-item" data-value="d4">d4</li>
<li class="list-group-item" data-value="d6">d6</li>
<li class="list-group-item" data-value="d8">d8</li>
<li class="list-group-item" data-value="d10">d10</li>
<li class="list-group-item" data-value="d12">d12</li>
<li class="list-group-item" data-value="d20">d20</li>
<li class="list-group-item" data-value="d100">d100</li>
<li class="list-group-item" data-value="custom">...</li>
</ul>
</div>`;
$('#extensionsMenu').prepend(buttonHtml);
$(document.body).append(dropdownHtml)
$('#dice_dropdown li').on('click', doDiceRoll);
const button = $('#roll_dice');
const dropdown = $('#dice_dropdown');
dropdown.hide();
button.hide();
let popper = Popper.createPopper(button.get(0), dropdown.get(0), {
placement: 'top',
});
$(document).on('click touchend', function (e) {
const target = $(e.target);
if (target.is(dropdown)) return;
if (target.is(button) && !dropdown.is(":visible")) {
e.preventDefault();
dropdown.fadeIn(250);
popper.update();
} else {
dropdown.fadeOut(250);
}
});
}
async function moduleWorker() {
$('#roll_dice').toggle(getContext().onlineStatus !== 'no_connection');
}
jQuery(function () {
addDiceRollButton();
moduleWorker();
setInterval(moduleWorker, UPDATE_INTERVAL);
registerSlashCommand('roll', (_, value) => doDiceRoll(value), ['r'], "<span class='monospace'>(dice formula)</span> roll the dice. For example, /roll 2d6", false, true);
});

View File

@@ -1,11 +0,0 @@
{
"display_name": "D&D Dice",
"loading_order": 5,
"requires": [],
"optional": [],
"js": "index.js",
"css": "style.css",
"author": "Cohee#1207",
"version": "1.0.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
}

View File

@@ -1,26 +0,0 @@
#roll_dice {
/* order: 100; */
/* width: 40px;
height: 40px;
margin: 0;
padding: 1px; */
outline: none;
border: none;
cursor: pointer;
transition: 0.3s;
opacity: 0.7;
display: flex;
align-items: center;
/* justify-content: center; */
}
#roll_dice:hover {
opacity: 1;
filter: brightness(1.2);
}
#dice_dropdown {
z-index: 30000;
backdrop-filter: blur(--SmartThemeBlurStrength);
}

View File

@@ -1,4 +1,4 @@
import { callPopup, eventSource, event_types, getRequestHeaders, saveSettingsDebounced } from "../../../script.js";
import { callPopup, eventSource, event_types, getRequestHeaders, saveSettingsDebounced, this_chid } from "../../../script.js";
import { dragElement, isMobile } from "../../RossAscends-mods.js";
import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplate } from "../../extensions.js";
import { loadMovingUIState, power_user } from "../../power-user.js";
@@ -493,24 +493,6 @@ async function moduleWorker() {
return;
}
// character changed
if (context.groupId !== lastCharacter && context.characterId !== lastCharacter) {
removeExpression();
spriteCache = {};
//clear expression
let imgElement = document.getElementById('expression-image');
if (imgElement && imgElement instanceof HTMLImageElement) {
imgElement.src = "";
}
//set checkbox to global var
$('#image_type_toggle').prop('checked', extension_settings.expressions.talkinghead);
if (extension_settings.expressions.talkinghead) {
setTalkingHeadState(extension_settings.expressions.talkinghead);
}
}
const vnMode = isVisualNovelMode();
const vnWrapperVisible = $('#visual-novel-wrapper').is(':visible');
@@ -531,7 +513,7 @@ async function moduleWorker() {
}
const currentLastMessage = getLastCharacterMessage();
let spriteFolderName = getSpriteFolderName(currentLastMessage, currentLastMessage.name);
let spriteFolderName = context.groupId ? getSpriteFolderName(currentLastMessage, currentLastMessage.name) : getSpriteFolderName();
// character has no expressions or it is not loaded
if (Object.keys(spriteCache).length === 0) {
@@ -782,7 +764,7 @@ function sampleClassifyText(text) {
// Remove asterisks and quotes
let result = text.replace(/[\*\"]/g, '');
const SAMPLE_THRESHOLD = 300;
const SAMPLE_THRESHOLD = 500;
const HALF_SAMPLE_THRESHOLD = SAMPLE_THRESHOLD / 2;
if (text.length < SAMPLE_THRESHOLD) {
@@ -1492,11 +1474,29 @@ function setExpressionOverrideHtml(forceClear = false) {
moduleWorker();
dragElement($("#expression-holder"))
eventSource.on(event_types.CHAT_CHANGED, () => {
// character changed
removeExpression();
spriteCache = {};
//clear expression
let imgElement = document.getElementById('expression-image');
if (imgElement && imgElement instanceof HTMLImageElement) {
imgElement.src = "";
}
//set checkbox to global var
$('#image_type_toggle').prop('checked', extension_settings.expressions.talkinghead);
if (extension_settings.expressions.talkinghead) {
setTalkingHeadState(extension_settings.expressions.talkinghead);
}
setExpressionOverrideHtml();
if (isVisualNovelMode()) {
$('#visual-novel-wrapper').empty();
}
updateFunction();
});
eventSource.on(event_types.MOVABLE_PANELS_RESET, updateVisualNovelModeDebounced);
eventSource.on(event_types.GROUP_UPDATED, updateVisualNovelModeDebounced);

View File

@@ -5,7 +5,7 @@ import {
getRequestHeaders,
} from "../../../script.js";
import { selected_group } from "../../group-chats.js";
import { loadFileToDocument } from "../../utils.js";
import { loadFileToDocument, delay } from "../../utils.js";
import { loadMovingUIState } from '../../power-user.js';
import { dragElement } from '../../RossAscends-mods.js';
import { registerSlashCommand } from "../../slash-commands.js";
@@ -109,6 +109,13 @@ async function initGallery(items, url) {
let file = e.originalEvent.dataTransfer.files[0];
uploadFile(file, url); // Added url parameter to know where to upload
});
//let images populate first
await delay(100)
//unset the height (which must be getting set by the gallery library at some point)
$("#dragGallery").css('height', 'unset');
//force a resize to make images display correctly
jQuery("#dragGallery").nanogallery2('resize');
}
/**
@@ -247,14 +254,16 @@ $(document).ready(function () {
* The cloned element has its attributes set, a new child div appended, and is made visible on the body.
* Additionally, it sets up the element to prevent dragging on its images.
*/
function makeMovable(id="gallery"){
function makeMovable(id = "gallery") {
console.debug('making new container from template')
const template = $('#generic_draggable_template').html();
const newElement = $(template);
newElement.css('background-color', 'var(--SmartThemeBlurTintColor)');
newElement.attr('forChar', id);
newElement.attr('id', `${id}`);
newElement.find('.drag-grabber').attr('id', `${id}header`);
newElement.find('.dragTitle').text('Image Gallery')
//add a div for the gallery
newElement.append(`<div id="dragGallery"></div>`);
// add no-scrollbar class to this element
@@ -326,6 +335,8 @@ function makeDragImg(id, url) {
// Ensure that the newly added element is displayed as block
draggableElem.style.display = 'block';
//and has no padding unlike other non-zoomed-avatar draggables
draggableElem.style.padding = '0';
// Add an id to the close button
// If the close button exists, set related-id
@@ -375,11 +386,11 @@ function makeDragImg(id, url) {
* @param {string} id - The ID to be sanitized.
* @returns {string} - The sanitized ID.
*/
function sanitizeHTMLId(id){
function sanitizeHTMLId(id) {
// Replace spaces and non-word characters
id = id.replace(/\s+/g, '-')
.replace(/[^\x00-\x7F]/g, '-')
.replace(/\W/g, '');
.replace(/[^\x00-\x7F]/g, '-')
.replace(/\W/g, '');
return id;
}

View File

@@ -1,210 +0,0 @@
import { eventSource, event_types, getRequestHeaders, is_send_press, saveSettingsDebounced } from "../../../script.js";
import { extension_settings, getContext, renderExtensionTemplate } from "../../extensions.js";
import { SECRET_KEYS, secret_state } from "../../secrets.js";
import { collapseNewlines } from "../../power-user.js";
import { bufferToBase64, debounce } from "../../utils.js";
import { decodeTextTokens, getTextTokens, tokenizers } from "../../tokenizers.js";
const MODULE_NAME = 'hypebot';
const WAITING_VERBS = ['thinking', 'typing', 'brainstorming', 'cooking', 'conjuring'];
const MAX_PROMPT = 1024;
const MAX_LENGTH = 50;
const MAX_STRING_LENGTH = MAX_PROMPT * 4;
const settings = {
enabled: false,
name: 'Goose',
};
/**
* Returns a random waiting verb
* @returns {string} Random waiting verb
*/
function getWaitingVerb() {
return WAITING_VERBS[Math.floor(Math.random() * WAITING_VERBS.length)];
}
/**
* Returns a random verb based on the text
* @param {string} text Text to generate a verb for
* @returns {string} Random verb
*/
function getVerb(text) {
let verbList = ['says', 'notes', 'states', 'whispers', 'murmurs', 'mumbles'];
if (text.endsWith('!')) {
verbList = ['proclaims', 'declares', 'salutes', 'exclaims', 'cheers'];
}
if (text.endsWith('?')) {
verbList = ['asks', 'suggests', 'ponders', 'wonders', 'inquires', 'questions'];
}
return verbList[Math.floor(Math.random() * verbList.length)];
}
/**
* Formats the HypeBot reply text
* @param {string} text HypeBot output text
* @returns {string} Formatted HTML text
*/
function formatReply(text) {
return `<span class="hypebot_name">${settings.name} ${getVerb(text)}:</span>&nbsp;<span class="hypebot_text">${text}</span>`;
}
let hypeBotBar;
let abortController;
const generateDebounced = debounce(() => generateHypeBot(), 500);
/**
* Sets the HypeBot text. Preserves scroll position of the chat.
* @param {string} text Text to set
*/
function setHypeBotText(text) {
const chatBlock = $('#chat');
const originalScrollBottom = chatBlock[0].scrollHeight - (chatBlock.scrollTop() + chatBlock.outerHeight());
hypeBotBar.html(DOMPurify.sanitize(text));
const newScrollTop = chatBlock[0].scrollHeight - (chatBlock.outerHeight() + originalScrollBottom);
chatBlock.scrollTop(newScrollTop);
}
/**
* Called when a chat event occurs to generate a HypeBot reply.
* @param {boolean} clear Clear the hypebot bar.
*/
function onChatEvent(clear) {
if (clear) {
setHypeBotText('');
}
abortController?.abort();
generateDebounced();
};
/**
* Generates a HypeBot reply.
*/
async function generateHypeBot() {
if (!settings.enabled || is_send_press) {
return;
}
if (!secret_state[SECRET_KEYS.NOVEL]) {
setHypeBotText('<div class="hypebot_nokey">No API key found. Please enter your API key in the NovelAI API Settings to use the HypeBot.</div>');
return;
}
console.debug('Generating HypeBot reply');
setHypeBotText(`<span class="hypebot_name">${settings.name}</span> is ${getWaitingVerb()}...`);
const context = getContext();
const chat = context.chat.slice();
let prompt = '';
for (let index = chat.length - 1; index >= 0; index--) {
const message = chat[index];
if (message.is_system || !message.mes) {
continue;
}
prompt = `\n${message.mes}\n${prompt}`;
if (prompt.length >= MAX_STRING_LENGTH) {
break;
}
}
prompt = collapseNewlines(prompt.replaceAll(/[\*\[\]\{\}]/g, ''));
if (!prompt) {
return;
}
const sliceLength = MAX_PROMPT - MAX_LENGTH;
const encoded = getTextTokens(tokenizers.GPT2, prompt).slice(-sliceLength);
// Add a stop string token to the end of the prompt
encoded.push(49527);
const base64String = await bufferToBase64(new Uint16Array(encoded).buffer);
const parameters = {
input: base64String,
model: "hypebot",
streaming: false,
temperature: 1,
max_length: MAX_LENGTH,
min_length: 1,
top_k: 0,
top_p: 1,
tail_free_sampling: 0.95,
repetition_penalty: 1,
repetition_penalty_range: 2048,
repetition_penalty_slope: 0.18,
repetition_penalty_frequency: 0,
repetition_penalty_presence: 0,
phrase_rep_pen: "off",
bad_words_ids: [],
stop_sequences: [[48585]],
generate_until_sentence: true,
use_cache: false,
use_string: false,
return_full_text: false,
prefix: "vanilla",
logit_bias_exp: [],
order: [0, 1, 2, 3],
};
abortController = new AbortController();
const response = await fetch('/api/novelai/generate', {
headers: getRequestHeaders(),
body: JSON.stringify(parameters),
method: 'POST',
signal: abortController.signal,
});
if (response.ok) {
const data = await response.json();
const ids = Array.from(new Uint16Array(Uint8Array.from(atob(data.output), c => c.charCodeAt(0)).buffer));
const output = decodeTextTokens(tokenizers.GPT2, ids).replace(/<2F>/g, '').trim();
setHypeBotText(formatReply(output));
} else {
setHypeBotText('<div class="hypebot_error">Something went wrong while generating a HypeBot reply. Please try again.</div>');
}
}
jQuery(() => {
if (!extension_settings.hypebot) {
extension_settings.hypebot = settings;
}
Object.assign(settings, extension_settings.hypebot);
$('#extensions_settings2').append(renderExtensionTemplate(MODULE_NAME, 'settings'));
hypeBotBar = $(`<div id="hypeBotBar"></div>`).toggle(settings.enabled);
$('#send_form').append(hypeBotBar);
$('#hypebot_enabled').prop('checked', settings.enabled).on('input', () => {
settings.enabled = $('#hypebot_enabled').prop('checked');
hypeBotBar.toggle(settings.enabled);
abortController?.abort();
Object.assign(extension_settings.hypebot, settings);
saveSettingsDebounced();
});
$('#hypebot_name').val(settings.name).on('input', () => {
settings.name = String($('#hypebot_name').val());
Object.assign(extension_settings.hypebot, settings);
saveSettingsDebounced();
});
eventSource.on(event_types.CHAT_CHANGED, () => onChatEvent(true));
eventSource.on(event_types.MESSAGE_DELETED, () => onChatEvent(true));
eventSource.on(event_types.MESSAGE_EDITED, () => onChatEvent(true));
eventSource.on(event_types.MESSAGE_SENT, () => onChatEvent(false));
eventSource.on(event_types.MESSAGE_RECEIVED, () => onChatEvent(false));
eventSource.on(event_types.MESSAGE_SWIPED, () => onChatEvent(false));
});

View File

@@ -1,11 +0,0 @@
{
"display_name": "HypeBot",
"loading_order": 1000,
"requires": [],
"optional": [],
"js": "index.js",
"css": "style.css",
"author": "Cohee#1207",
"version": "1.0.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
}

View File

@@ -1,18 +0,0 @@
<div class="hypebot_settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>HypeBot</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<div>Show personalized suggestions based on your recent chats using the NovelAI's HypeBot engine.</div>
<small><i>Hint: Save an API key in the NovelAI API settings to use it here.</i></small>
<label class="checkbox_label" for="hypebot_enabled">
<input id="hypebot_enabled" type="checkbox" class="checkbox">
Enabled
</label>
<label>Name:</label>
<input id="hypebot_name" type="text" class="text_pole" placeholder="Goose">
</div>
</div>
</div>

View File

@@ -1,17 +0,0 @@
#hypeBotBar {
width: 100%;
max-width: 100%;
padding: 0.5em;
white-space: normal;
font-size: calc(var(--mainFontSize) * 0.85);
order: 20;
}
.hypebot_nokey {
text-align: center;
font-style: italic;
}
.hypebot_name {
font-weight: 600;
}

View File

@@ -1,54 +0,0 @@
<div class="idle-settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header" title="Indicates the settings for the idle feature.">
<b>Idle</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<div class="idle_block flex-container">
<input id="idle_enabled" type="checkbox" title="Toggle to enable or disable the idle feature." />
<label for="idle_enabled">Enabled</label>
</div>
<div class="idle_block flex-container">
<input id="idle_repeats" class="text_pole widthUnset" type="number" min="0" max="100000" step="1" title="The number of times the idle action will be prompted." />
<label for="idle_repeats">Idle Prompt Count</label>
</div>
<div class="idle_block flex-container" style="display: none;">
<input id="idle_timer_min" class="text_pole widthUnset" type="number" min="0" max="600000" step="1" title="The minimum amount of time in seconds before the idle action is triggered." />
<label for="idle_timer_min">Idle Timer Minimum (seconds)</label>
</div>
<div class="idle_block flex-container">
<input id="idle_timer" class="text_pole widthUnset" type="number" min="0" max="600000" step="1" title="The amount of time in seconds before the idle action is triggered." />
<label for="idle_timer">Idle Timer (seconds)</label>
</div>
<div class="idle_block flex-container">
<label for="idle_prompts">Idle Prompts</label>
<textarea id="idle_prompts" class="text_pole textarea_compact" rows="6" title="The prompts to be sent to initial the idle reply (newline seperated)."></textarea>
</div>
<div class="idle_block flex-container">
<input id="idle_use_continuation" type="checkbox" title="Indicates whether the idle action will just use the 'Continue' function instead of a prompt." />
<label for="idle_use_continuation">Use Continuation</label>
</div>
<div class="idle_block flex-container">
<input id="idle_random_time" type="checkbox" title="Indicates if the idle time should be randomized between a min/max value." />
<label for="idle_random_time">Randomize Time</label>
</div>
<div class="idle_block flex-container">
<input id="idle_include_prompt" type="checkbox" title="Indicates if the idle prompting should be included in context. (Sends as user)" />
<label for="idle_include_prompt">Include Idle Prompt</label>
</div>
<div class="idle_block flex-container">
<label for="idle_sendAs">Send As</label>
<select id="idle_sendAs" class="text_pole" title="Determines how the idle message prompting is sent; as a user, character, system, or raw message.">
<option value="user">User</option>
<option value="char">Character</option>
<option value="sys">System</option>
<option value="raw">Raw</option>
</select>
</div>
<hr class="sysHR" />
</div>
</div>
</div>

View File

@@ -1,329 +0,0 @@
import {
saveSettingsDebounced,
substituteParams
} from "../../../script.js";
import { debounce } from "../../utils.js";
import { promptQuietForLoudResponse, sendMessageAs, sendNarratorMessage } from "../../slash-commands.js";
import { extension_settings, getContext, renderExtensionTemplate } from "../../extensions.js";
import { registerSlashCommand } from "../../slash-commands.js";
const extensionName = "idle";
let idleTimer = null;
let repeatCount = 0;
let defaultSettings = {
enabled: false,
timer: 120,
prompts: [
"*stands silently, looking deep in thought*",
"*pauses, eyes wandering over the surroundings*",
"*hesitates, appearing lost for a moment*",
"*takes a deep breath, collecting their thoughts*",
"*gazes into the distance, seemingly distracted*",
"*remains still, absorbing the ambiance*",
"*lingers in silence, a contemplative look on their face*",
"*stops, fingers brushing against an old memory*",
"*seems to drift into a momentary daydream*",
"*waits quietly, allowing the weight of the moment to settle*",
],
useContinuation: true,
repeats: 2, // 0 = infinite
sendAs: "user",
randomTime: false,
timeMin: 60,
includePrompt: false,
};
//TODO: Can we make this a generic function?
/**
* Load the extension settings and set defaults if they don't exist.
*/
async function loadSettings() {
if (!extension_settings.idle) {
console.log("Creating extension_settings.idle");
extension_settings.idle = {};
}
for (const [key, value] of Object.entries(defaultSettings)) {
if (!extension_settings.idle.hasOwnProperty(key)) {
console.log(`Setting default for: ${key}`);
extension_settings.idle[key] = value;
}
}
populateUIWithSettings();
}
//TODO: Can we make this a generic function too?
/**
* Populate the UI components with values from the extension settings.
*/
function populateUIWithSettings() {
$("#idle_timer").val(extension_settings.idle.timer).trigger("input");
$("#idle_prompts").val(extension_settings.idle.prompts.join("\n")).trigger("input");
$("#idle_use_continuation").prop("checked", extension_settings.idle.useContinuation).trigger("input");
$("#idle_enabled").prop("checked", extension_settings.idle.enabled).trigger("input");
$("#idle_repeats").val(extension_settings.idle.repeats).trigger("input");
$("#idle_sendAs").val(extension_settings.idle.sendAs).trigger("input");
$("#idle_random_time").prop("checked", extension_settings.idle.randomTime).trigger("input");
$("#idle_timer_min").val(extension_settings.idle.timerMin).trigger("input");
$("#idle_include_prompt").prop("checked", extension_settings.idle.includePrompt).trigger("input");
}
/**
* Reset the idle timer based on the extension settings and context.
*/
function resetIdleTimer() {
console.debug("Resetting idle timer");
if (idleTimer) clearTimeout(idleTimer);
let context = getContext();
if (!context.characterId && !context.groupID) return;
if (!extension_settings.idle.enabled) return;
if (extension_settings.idle.randomTime) {
// ensure these are ints
let min = extension_settings.idle.timerMin;
let max = extension_settings.idle.timer;
min = parseInt(min);
max = parseInt(max);
let randomTime = (Math.random() * (max - min + 1)) + min;
idleTimer = setTimeout(sendIdlePrompt, 1000 * randomTime);
} else {
idleTimer = setTimeout(sendIdlePrompt, 1000 * extension_settings.idle.timer);
}
}
/**
* Send a random idle prompt to the AI based on the extension settings.
* Checks conditions like if the extension is enabled and repeat conditions.
*/
async function sendIdlePrompt() {
if (!extension_settings.idle.enabled) return;
// Check repeat conditions and waiting for a response
if (repeatCount >= extension_settings.idle.repeats || $('#mes_stop').is(':visible')) {
//console.debug("Not sending idle prompt due to repeat conditions or waiting for a response.");
resetIdleTimer();
return;
}
const randomPrompt = extension_settings.idle.prompts[
Math.floor(Math.random() * extension_settings.idle.prompts.length)
];
sendPrompt(randomPrompt);
repeatCount++;
resetIdleTimer();
}
/**
* Add our prompt to the chat and then send the chat to the backend.
* @param {string} sendAs - The type of message to send. "user", "char", or "sys".
* @param {string} prompt - The prompt text to send to the AI.
*/
function sendLoud(sendAs, prompt) {
if (sendAs === "user") {
prompt = substituteParams(prompt);
$("#send_textarea").val(prompt);
// Set the focus back to the textarea
$("#send_textarea").focus();
$("#send_but").trigger('click');
} else if (sendAs === "char") {
sendMessageAs("", `${getContext().name2}\n${prompt}`);
promptQuietForLoudResponse(sendAs, "");
} else if (sendAs === "sys") {
sendNarratorMessage("", prompt);
promptQuietForLoudResponse(sendAs, "");
}
else {
console.error(`Unknown sendAs value: ${sendAs}`);
}
}
/**
* Send the provided prompt to the AI. Determines method based on continuation setting.
* @param {string} prompt - The prompt text to send to the AI.
*/
function sendPrompt(prompt) {
clearTimeout(idleTimer);
$("#send_textarea").off("input");
if (extension_settings.idle.useContinuation) {
$('#option_continue').trigger('click');
console.debug("Sending idle prompt with continuation");
} else {
console.debug("Sending idle prompt");
console.log(extension_settings.idle);
if (extension_settings.idle.includePrompt) {
sendLoud(extension_settings.idle.sendAs, prompt);
}
else {
promptQuietForLoudResponse(extension_settings.idle.sendAs, prompt);
}
}
}
/**
* Load the settings HTML and append to the designated area.
*/
async function loadSettingsHTML() {
const settingsHtml = renderExtensionTemplate(extensionName, "dropdown");
$("#extensions_settings2").append(settingsHtml);
}
/**
* Update a specific setting based on user input.
* @param {string} elementId - The HTML element ID tied to the setting.
* @param {string} property - The property name in the settings object.
* @param {boolean} [isCheckbox=false] - Whether the setting is a checkbox.
*/
function updateSetting(elementId, property, isCheckbox = false) {
let value = $(`#${elementId}`).val();
if (isCheckbox) {
value = $(`#${elementId}`).prop('checked');
}
if (property === "prompts") {
value = value.split("\n");
}
extension_settings.idle[property] = value;
saveSettingsDebounced();
}
/**
* Attach an input listener to a UI component to update the corresponding setting.
* @param {string} elementId - The HTML element ID tied to the setting.
* @param {string} property - The property name in the settings object.
* @param {boolean} [isCheckbox=false] - Whether the setting is a checkbox.
*/
function attachUpdateListener(elementId, property, isCheckbox = false) {
$(`#${elementId}`).on('input', debounce(() => {
updateSetting(elementId, property, isCheckbox);
}, 250));
}
/**
* Handle the enabling or disabling of the idle extension.
* Adds or removes the idle listeners based on the checkbox's state.
*/
function handleIdleEnabled() {
if (!extension_settings.idle.enabled) {
clearTimeout(idleTimer);
removeIdleListeners();
} else {
resetIdleTimer();
attachIdleListeners();
}
}
/**
* Setup input listeners for the various settings and actions related to the idle extension.
*/
function setupListeners() {
const settingsToWatch = [
['idle_timer', 'timer'],
['idle_prompts', 'prompts'],
['idle_use_continuation', 'useContinuation', true],
['idle_enabled', 'enabled', true],
['idle_repeats', 'repeats'],
['idle_sendAs', 'sendAs'],
['idle_random_time', 'randomTime', true],
['idle_timer_min', 'timerMin'],
['idle_include_prompt', 'includePrompt', true]
];
settingsToWatch.forEach(setting => {
attachUpdateListener(...setting);
});
// Idleness listeners, could be made better
$('#idle_enabled').on('input', debounce(handleIdleEnabled, 250));
// Add the idle listeners initially if the idle feature is enabled
if (extension_settings.idle.enabled) {
attachIdleListeners();
}
//show/hide timer min parent div
$('#idle_random_time').on('input', function () {
if ($(this).prop('checked')) {
$('#idle_timer_min').parent().show();
} else {
$('#idle_timer_min').parent().hide();
}
$('#idle_timer').trigger('input');
});
// if we're including the prompt, hide raw from the sendAs dropdown
$('#idle_include_prompt').on('input', function () {
if ($(this).prop('checked')) {
$('#idle_sendAs option[value="raw"]').hide();
} else {
$('#idle_sendAs option[value="raw"]').show();
}
});
//make sure timer min is less than timer
$('#idle_timer').on('input', function () {
if ($('#idle_random_time').prop('checked')) {
if ($(this).val() < $('#idle_timer_min').val()) {
$('#idle_timer_min').val($(this).val());
$('#idle_timer_min').trigger('input');
}
}
});
}
const debouncedActivityHandler = debounce((event) => {
// Check if the event target (or any of its parents) has the id "option_continue"
if ($(event.target).closest('#option_continue').length) {
return; // Do not proceed if the click was on (or inside) an element with id "option_continue"
}
console.debug("Activity detected, resetting idle timer");
resetIdleTimer();
repeatCount = 0;
}, 250);
function attachIdleListeners() {
$(document).on("click keypress", debouncedActivityHandler);
document.addEventListener('keydown', debouncedActivityHandler);
}
/**
* Remove idle-specific listeners.
*/
function removeIdleListeners() {
$(document).off("click keypress", debouncedActivityHandler);
document.removeEventListener('keydown', debouncedActivityHandler);
}
function toggleIdle() {
extension_settings.idle.enabled = !extension_settings.idle.enabled;
$('#idle_enabled').prop('checked', extension_settings.idle.enabled);
$('#idle_enabled').trigger('input');
toastr.info(`Idle mode ${extension_settings.idle.enabled ? "enabled" : "disabled"}.`);
resetIdleTimer();
}
jQuery(async () => {
await loadSettingsHTML();
loadSettings();
setupListeners();
if (extension_settings.idle.enabled) {
resetIdleTimer();
}
// once the doc is ready, check if random time is checked and hide/show timer min
if ($('#idle_random_time').prop('checked')) {
$('#idle_timer_min').parent().show();
}
registerSlashCommand('idle', toggleIdle, [], ' toggles idle mode', true, true);
});

View File

@@ -1,12 +0,0 @@
{
"display_name": "Idle",
"loading_order": 6,
"requires": [],
"optional": [
],
"js": "index.js",
"css": "style.css",
"author": "City-Unit",
"version": "1.0.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
}

View File

@@ -1,3 +0,0 @@
.idle_block {
align-items: center;
}

View File

@@ -1,949 +0,0 @@
import { saveSettingsDebounced, getCurrentChatId, system_message_types, extension_prompt_types, eventSource, event_types, getRequestHeaders, substituteParams, } from "../../../script.js";
import { humanizedDateTime } from "../../RossAscends-mods.js";
import { getApiUrl, extension_settings, getContext, doExtrasFetch } from "../../extensions.js";
import { CHARACTERS_PER_TOKEN_RATIO } from "../../tokenizers.js";
import { getFileText, onlyUnique, splitRecursive } from "../../utils.js";
export { MODULE_NAME };
const MODULE_NAME = 'chromadb';
const dbStore = localforage.createInstance({ name: 'SillyTavern_ChromaDB' });
const defaultSettings = {
strategy: 'original',
sort_strategy: 'date',
keep_context: 10,
keep_context_min: 1,
keep_context_max: 500,
keep_context_step: 1,
n_results: 20,
n_results_min: 0,
n_results_max: 500,
n_results_step: 1,
chroma_depth: 20,
chroma_depth_min: -1,
chroma_depth_max: 500,
chroma_depth_step: 1,
chroma_default_msg: "In a past conversation: [{{memories}}]",
chroma_default_hhaa_wrapper: "Previous messages exchanged between {{user}} and {{char}}:\n{{memories}}",
chroma_default_hhaa_memory: "- {{name}}: {{message}}\n",
hhaa_token_limit: 512,
split_length: 384,
split_length_min: 64,
split_length_max: 4096,
split_length_step: 64,
file_split_length: 1024,
file_split_length_min: 512,
file_split_length_max: 4096,
file_split_length_step: 128,
keep_context_proportion: 0.5,
keep_context_proportion_min: 0.0,
keep_context_proportion_max: 1.0,
keep_context_proportion_step: 0.05,
auto_adjust: true,
freeze: false,
query_last_only: true,
};
const postHeaders = {
'Content-Type': 'application/json',
'Bypass-Tunnel-Reminder': 'bypass',
};
async function invalidateMessageSyncState(messageId) {
console.log('CHROMADB: invalidating message sync state', messageId);
const state = await getChatSyncState();
state[messageId] = 0;
await dbStore.setItem(getCurrentChatId(), state);
}
async function getChatSyncState() {
const currentChatId = getCurrentChatId();
if (!checkChatId(currentChatId)) {
return;
}
const context = getContext();
const chatState = (await dbStore.getItem(currentChatId)) || [];
// if the chat length has decreased, it means that some messages were deleted
if (chatState.length > context.chat.length) {
for (let i = context.chat.length; i < chatState.length; i++) {
// if the synced message was deleted, notify the user
if (chatState[i]) {
toastr.warning(
'Purge your ChromaDB to remove it from there too. See the "Smart Context" tab in the Extensions menu for more information.',
'Message deleted from chat, but it still exists inside the ChromaDB database.',
{ timeOut: 0, extendedTimeOut: 0, preventDuplicates: true },
);
break;
}
}
}
chatState.length = context.chat.length;
for (let i = 0; i < chatState.length; i++) {
if (chatState[i] === undefined) {
chatState[i] = 0;
}
}
await dbStore.setItem(currentChatId, chatState);
return chatState;
}
async function loadSettings() {
if (Object.keys(extension_settings.chromadb).length === 0) {
Object.assign(extension_settings.chromadb, defaultSettings);
}
console.debug(`loading chromadb strat:${extension_settings.chromadb.strategy}`);
$("#chromadb_strategy option[value=" + extension_settings.chromadb.strategy + "]").attr(
"selected",
"true"
);
$("#chromadb_sort_strategy option[value=" + extension_settings.chromadb.sort_strategy + "]").attr(
"selected",
"true"
);
$('#chromadb_keep_context').val(extension_settings.chromadb.keep_context).trigger('input');
$('#chromadb_n_results').val(extension_settings.chromadb.n_results).trigger('input');
$('#chromadb_split_length').val(extension_settings.chromadb.split_length).trigger('input');
$('#chromadb_file_split_length').val(extension_settings.chromadb.file_split_length).trigger('input');
$('#chromadb_keep_context_proportion').val(extension_settings.chromadb.keep_context_proportion).trigger('input');
$('#chromadb_custom_depth').val(extension_settings.chromadb.chroma_depth).trigger('input');
$('#chromadb_custom_msg').val(extension_settings.chromadb.recall_msg).trigger('input');
$('#chromadb_hhaa_wrapperfmt').val(extension_settings.chromadb.hhaa_wrapper_msg).trigger('input');
$('#chromadb_hhaa_memoryfmt').val(extension_settings.chromadb.hhaa_memory_msg).trigger('input');
$('#chromadb_hhaa_token_limit').val(extension_settings.chromadb.hhaa_token_limit).trigger('input');
$('#chromadb_auto_adjust').prop('checked', extension_settings.chromadb.auto_adjust);
$('#chromadb_freeze').prop('checked', extension_settings.chromadb.freeze);
$('#chromadb_query_last_only').prop('checked', extension_settings.chromadb.query_last_only);
enableDisableSliders();
onStrategyChange();
}
function onStrategyChange() {
console.debug('changing chromadb strat');
extension_settings.chromadb.strategy = $('#chromadb_strategy').val();
if (extension_settings.chromadb.strategy === "custom") {
$('#chromadb_custom_depth').show();
$('label[for="chromadb_custom_depth"]').show();
$('#chromadb_custom_msg').show();
$('label[for="chromadb_custom_msg"]').show();
}
else if(extension_settings.chromadb.strategy === "hh_aa"){
$('#chromadb_hhaa_wrapperfmt').show();
$('label[for="chromadb_hhaa_wrapperfmt"]').show();
$('#chromadb_hhaa_memoryfmt').show();
$('label[for="chromadb_hhaa_memoryfmt"]').show();
$('#chromadb_hhaa_token_limit').show();
$('label[for="chromadb_hhaa_token_limit"]').show();
}
saveSettingsDebounced();
}
function onRecallStrategyChange() {
console.log('changing chromadb recall strat');
extension_settings.chromadb.recall_strategy = $('#chromadb_recall_strategy').val();
saveSettingsDebounced();
}
function onSortStrategyChange() {
console.log('changing chromadb sort strat');
extension_settings.chromadb.sort_strategy = $('#chromadb_sort_strategy').val();
saveSettingsDebounced();
}
function onKeepContextInput() {
extension_settings.chromadb.keep_context = Number($('#chromadb_keep_context').val());
$('#chromadb_keep_context_value').text(extension_settings.chromadb.keep_context);
saveSettingsDebounced();
}
function onNResultsInput() {
extension_settings.chromadb.n_results = Number($('#chromadb_n_results').val());
$('#chromadb_n_results_value').text(extension_settings.chromadb.n_results);
saveSettingsDebounced();
}
function onChromaDepthInput() {
extension_settings.chromadb.chroma_depth = Number($('#chromadb_custom_depth').val());
$('#chromadb_custom_depth_value').text(extension_settings.chromadb.chroma_depth);
saveSettingsDebounced();
}
function onChromaMsgInput() {
extension_settings.chromadb.recall_msg = $('#chromadb_custom_msg').val();
saveSettingsDebounced();
}
function onChromaHHAAWrapper() {
extension_settings.chromadb.hhaa_wrapper_msg = $('#chromadb_hhaa_wrapperfmt').val();
saveSettingsDebounced();
}
function onChromaHHAAMemory() {
extension_settings.chromadb.hhaa_memory_msg = $('#chromadb_hhaa_memoryfmt').val();
saveSettingsDebounced();
}
function onChromaHHAATokens() {
extension_settings.chromadb.hhaa_token_limit = Number($('#chromadb_hhaa_token_limit').val());
$('#chromadb_hhaa_token_limit_value').text(extension_settings.chromadb.hhaa_token_limit);
saveSettingsDebounced();
}
function onSplitLengthInput() {
extension_settings.chromadb.split_length = Number($('#chromadb_split_length').val());
$('#chromadb_split_length_value').text(extension_settings.chromadb.split_length);
saveSettingsDebounced();
}
function onFileSplitLengthInput() {
extension_settings.chromadb.file_split_length = Number($('#chromadb_file_split_length').val());
$('#chromadb_file_split_length_value').text(extension_settings.chromadb.file_split_length);
saveSettingsDebounced();
}
function onChunkNLInput() {
let shouldSplit = $('#onChunkNLInput').is(':checked');
if (shouldSplit) {
extension_settings.chromadb.file_split_type = "newline";
} else {
extension_settings.chromadb.file_split_type = "length";
}
saveSettingsDebounced();
}
function checkChatId(chat_id) {
if (!chat_id || chat_id.trim() === '') {
toastr.error('Please select a character and try again.');
return false;
}
return true;
}
async function addMessages(chat_id, messages) {
if (extension_settings.chromadb.freeze) {
return { count: 0 };
}
const url = new URL(getApiUrl());
url.pathname = '/api/chromadb';
const messagesDeepCopy = JSON.parse(JSON.stringify(messages));
let splitMessages = [];
let id = 0;
messagesDeepCopy.forEach((m, index) => {
const split = splitRecursive(m.mes, extension_settings.chromadb.split_length);
splitMessages.push(...split.map(text => ({
...m,
mes: text,
send_date: id,
id: `msg-${id++}`,
index: index,
extra: undefined,
})));
});
splitMessages = await filterSyncedMessages(splitMessages);
// no messages to add
if (splitMessages.length === 0) {
return { count: 0 };
}
const transformedMessages = splitMessages.map((m) => ({
id: m.id,
role: m.is_user ? 'user' : 'assistant',
content: m.mes,
date: m.send_date,
meta: JSON.stringify(m),
}));
const addMessagesResult = await doExtrasFetch(url, {
method: 'POST',
headers: postHeaders,
body: JSON.stringify({ chat_id, messages: transformedMessages }),
});
if (addMessagesResult.ok) {
const addMessagesData = await addMessagesResult.json();
return addMessagesData; // { count: 1 }
}
return { count: 0 };
}
async function filterSyncedMessages(splitMessages) {
const syncState = await getChatSyncState();
const removeIndices = [];
const syncedIndices = [];
for (let i = 0; i < splitMessages.length; i++) {
const index = splitMessages[i].index;
if (syncState[index]) {
removeIndices.push(i);
continue;
}
syncedIndices.push(index);
}
for (const index of syncedIndices) {
syncState[index] = 1;
}
console.debug('CHROMADB: sync state', syncState.map((v, i) => ({ id: i, synced: v })));
await dbStore.setItem(getCurrentChatId(), syncState);
// remove messages that are already synced
return splitMessages.filter((_, i) => !removeIndices.includes(i));
}
async function onPurgeClick() {
const chat_id = getCurrentChatId();
if (!checkChatId(chat_id)) {
return;
}
const url = new URL(getApiUrl());
url.pathname = '/api/chromadb/purge';
const purgeResult = await doExtrasFetch(url, {
method: 'POST',
headers: postHeaders,
body: JSON.stringify({ chat_id }),
});
if (purgeResult.ok) {
await dbStore.removeItem(chat_id);
toastr.success('ChromaDB context has been successfully cleared');
}
}
async function onExportClick() {
const currentChatId = getCurrentChatId();
if (!checkChatId(currentChatId)) {
return;
}
const url = new URL(getApiUrl());
url.pathname = '/api/chromadb/export';
const exportResult = await doExtrasFetch(url, {
method: 'POST',
headers: postHeaders,
body: JSON.stringify({ chat_id: currentChatId }),
});
if (exportResult.ok) {
const data = await exportResult.json();
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const href = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = href;
link.download = currentChatId + '.json';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} else {
//Show the error from the result without the html, only what's in the body paragraph
let parser = new DOMParser();
let error = await exportResult.text();
let doc = parser.parseFromString(error, 'text/html');
let errorMessage = doc.querySelector('p').textContent;
toastr.error(`An error occurred while attempting to download the data from ChromaDB: ${errorMessage}`);
}
}
function tinyhash(text) {
let hash = 0;
for (let i = 0; i < text.length; ++i) {
hash = ((hash<<5) - hash) + text.charCodeAt(i);
hash = hash & hash; // Keeps it 32-bit allegedly.
}
return hash;
}
async function onSelectImportFile(e) {
const file = e.target.files[0];
const currentChatId = getCurrentChatId();
if (!checkChatId(currentChatId)) {
return;
}
if (!file) {
return;
}
try {
toastr.info('This may take some time, depending on the file size', 'Processing...');
const text = await getFileText(file);
const imported = JSON.parse(text);
const id_salt = "-" + tinyhash(imported.chat_id).toString(36);
for (let entry of imported.content) {
entry.id = entry.id + id_salt;
}
imported.chat_id = currentChatId;
const url = new URL(getApiUrl());
url.pathname = '/api/chromadb/import';
const importResult = await doExtrasFetch(url, {
method: 'POST',
headers: postHeaders,
body: JSON.stringify(imported),
});
if (importResult.ok) {
const importResultData = await importResult.json();
toastr.success(`Number of chunks: ${importResultData.count}`, 'Injected successfully!');
return importResultData;
} else {
throw new Error();
}
}
catch (error) {
console.log(error);
toastr.error('Something went wrong while importing the data');
}
finally {
e.target.form.reset();
}
}
async function queryMessages(chat_id, query) {
const url = new URL(getApiUrl());
url.pathname = '/api/chromadb/query';
const queryMessagesResult = await doExtrasFetch(url, {
method: 'POST',
headers: postHeaders,
body: JSON.stringify({ chat_id, query, n_results: extension_settings.chromadb.n_results }),
});
if (queryMessagesResult.ok) {
const queryMessagesData = await queryMessagesResult.json();
return queryMessagesData;
}
return [];
}
async function queryMultiMessages(chat_id, query) {
const context = getContext();
const response = await fetch("/getallchatsofcharacter", {
method: 'POST',
body: JSON.stringify({ avatar_url: context.characters[context.characterId].avatar }),
headers: getRequestHeaders(),
});
if (!response.ok) {
return;
}
let data = await response.json();
data = Object.values(data);
let chat_list = data.sort((a, b) => a["file_name"].localeCompare(b["file_name"])).reverse();
// Extracting chat_ids from the chat_list
chat_list = chat_list.map(chat => chat.file_name.replace(/\.[^/.]+$/, ""));
const url = new URL(getApiUrl());
url.pathname = '/api/chromadb/multiquery';
const queryMessagesResult = await fetch(url, {
method: 'POST',
body: JSON.stringify({ chat_list, query, n_results: extension_settings.chromadb.n_results }),
headers: postHeaders,
});
if (queryMessagesResult.ok) {
const queryMessagesData = await queryMessagesResult.json();
return queryMessagesData;
}
return [];
}
async function onSelectInjectFile(e) {
const file = e.target.files[0];
const currentChatId = getCurrentChatId();
if (!checkChatId(currentChatId)) {
return;
}
if (!file) {
return;
}
try {
toastr.info('This may take some time, depending on the file size', 'Processing...');
const text = await getFileText(file);
extension_settings.chromadb.file_split_type = "newline";
//allow splitting on newlines or splitrecursively
let split = [];
if (extension_settings.chromadb.file_split_type == "newline") {
split = text.split(/\r?\n/).filter(onlyUnique);
} else {
split = splitRecursive(text, extension_settings.chromadb.file_split_length).filter(onlyUnique);
}
const baseDate = Date.now();
const messages = split.map((m, i) => ({
id: `${file.name}-${split.indexOf(m)}`,
role: 'system',
content: m,
date: baseDate + i,
meta: JSON.stringify({
name: file.name,
is_user: false,
is_system: false,
send_date: humanizedDateTime(),
mes: m,
extra: {
type: system_message_types.NARRATOR,
}
}),
}));
const url = new URL(getApiUrl());
url.pathname = '/api/chromadb';
const addMessagesResult = await doExtrasFetch(url, {
method: 'POST',
headers: postHeaders,
body: JSON.stringify({ chat_id: currentChatId, messages: messages }),
});
if (addMessagesResult.ok) {
const addMessagesData = await addMessagesResult.json();
toastr.success(`Number of chunks: ${addMessagesData.count}`, 'Injected successfully!');
return addMessagesData;
} else {
throw new Error();
}
}
catch (error) {
console.log(error);
toastr.error('Something went wrong while injecting the data');
}
finally {
e.target.form.reset();
}
}
// Gets the length of character description in the current context
function getCharacterDataLength() {
const context = getContext();
const character = context.characters[context.characterId];
if (typeof character?.data !== 'object') {
return 0;
}
let characterDataLength = 0;
for (const [key, value] of Object.entries(character.data)) {
if (typeof value !== 'string') {
continue;
}
if (['description', 'personality', 'scenario'].includes(key)) {
characterDataLength += character.data[key].length;
}
}
return characterDataLength;
}
/*
* Automatically adjusts the extension settings for the optimal number of messages to keep and query based
* on the chat history and a specified maximum context length.
*/
function doAutoAdjust(chat, maxContext) {
// Only valid for chat injections strategy
if (extension_settings.chromadb.recall_strategy !== 0) {
return;
}
console.debug('CHROMADB: Auto-adjusting sliders (messages: %o, maxContext: %o)', chat.length, maxContext);
// Get mean message length
const meanMessageLength = chat.reduce((acc, cur) => acc + (cur?.mes?.length ?? 0), 0) / chat.length;
if (Number.isNaN(meanMessageLength) || meanMessageLength === 0) {
console.debug('CHROMADB: Mean message length is zero or NaN, aborting auto-adjust');
return;
}
// Adjust max context for character defs length
maxContext = Math.floor(maxContext - (getCharacterDataLength() / CHARACTERS_PER_TOKEN_RATIO));
console.debug('CHROMADB: Max context adjusted for character defs: %o', maxContext);
console.debug('CHROMADB: Mean message length (characters): %o', meanMessageLength);
// Convert to number of "tokens"
const meanMessageLengthTokens = Math.ceil(meanMessageLength / CHARACTERS_PER_TOKEN_RATIO);
console.debug('CHROMADB: Mean message length (tokens): %o', meanMessageLengthTokens);
// Get number of messages in context
const contextMessages = Math.max(1, Math.ceil(maxContext / meanMessageLengthTokens));
// Round up to nearest 5
const contextMessagesRounded = Math.ceil(contextMessages / 5) * 5;
console.debug('CHROMADB: Estimated context messages (rounded): %o', contextMessagesRounded);
// Messages to keep (proportional, rounded to nearest 5, minimum 5, maximum 500)
const messagesToKeep = Math.min(defaultSettings.keep_context_max, Math.max(5, Math.floor(contextMessagesRounded * extension_settings.chromadb.keep_context_proportion / 5) * 5));
console.debug('CHROMADB: Estimated messages to keep: %o', messagesToKeep);
// Messages to query (rounded, maximum 500)
const messagesToQuery = Math.min(defaultSettings.n_results_max, contextMessagesRounded - messagesToKeep);
console.debug('CHROMADB: Estimated messages to query: %o', messagesToQuery);
// Set extension settings
extension_settings.chromadb.keep_context = messagesToKeep;
extension_settings.chromadb.n_results = messagesToQuery;
// Update sliders
$('#chromadb_keep_context').val(messagesToKeep);
$('#chromadb_n_results').val(messagesToQuery);
// Update labels
$('#chromadb_keep_context_value').text(extension_settings.chromadb.keep_context);
$('#chromadb_n_results_value').text(extension_settings.chromadb.n_results);
}
window.chromadb_interceptGeneration = async (chat, maxContext) => {
if (extension_settings.chromadb.auto_adjust) {
doAutoAdjust(chat, maxContext);
}
const currentChatId = getCurrentChatId();
if (!currentChatId)
return;
//log the current settings
console.debug("CHROMADB: Current settings: %o", extension_settings.chromadb);
const selectedStrategy = extension_settings.chromadb.strategy;
const recallStrategy = extension_settings.chromadb.recall_strategy;
let recallMsg = extension_settings.chromadb.recall_msg || defaultSettings.chroma_default_msg;
const chromaDepth = extension_settings.chromadb.chroma_depth;
const chromaSortStrategy = extension_settings.chromadb.sort_strategy;
const chromaQueryLastOnly = extension_settings.chromadb.query_last_only;
const messagesToStore = chat.slice(0, -extension_settings.chromadb.keep_context);
if (messagesToStore.length > 0 && !extension_settings.chromadb.freeze) {
//log the messages to store
console.debug("CHROMADB: Messages to store: %o", messagesToStore);
//log the messages to store length vs keep context
console.debug("CHROMADB: Messages to store length vs keep context: %o vs %o", messagesToStore.length, extension_settings.chromadb.keep_context);
await addMessages(currentChatId, messagesToStore);
}
const lastMessage = chat[chat.length - 1];
let queriedMessages;
if (lastMessage) {
let queryBlob = "";
if (chromaQueryLastOnly) {
queryBlob = lastMessage.mes;
}
else {
for (let msg of chat.slice(-extension_settings.chromadb.keep_context)) {
queryBlob += `${msg.mes}\n`
}
}
console.debug("CHROMADB: Query text:", queryBlob);
if (recallStrategy === 'multichat') {
console.log("Utilizing multichat")
queriedMessages = await queryMultiMessages(currentChatId, queryBlob);
}
else {
queriedMessages = await queryMessages(currentChatId, queryBlob);
}
if (chromaSortStrategy === "date") {
queriedMessages.sort((a, b) => a.date - b.date);
}
else {
queriedMessages.sort((a, b) => b.distance - a.distance);
}
console.debug("CHROMADB: Query results: %o", queriedMessages);
let newChat = [];
if (selectedStrategy === 'ross') {
//adds chroma to the end of chat and allows Generate() to cull old messages naturally.
const context = getContext();
const charname = context.name2;
newChat.push(
{
is_user: false,
mes: `[Use these past chat exchanges to inform ${charname}'s next response:`,
name: "system",
send_date: 0,
}
);
newChat.push(...queriedMessages.map(m => m.meta).filter(onlyUnique).map(JSON.parse));
newChat.push(
{
is_user: false,
mes: `]\n`,
name: "system",
send_date: 0,
}
);
chat.splice(chat.length, 0, ...newChat);
}
if (selectedStrategy === 'hh_aa') {
// Insert chroma history messages as a list at the AFTER_SCENARIO anchor point
const context = getContext();
const chromaTokenLimit = extension_settings.chromadb.hhaa_token_limit;
let wrapperMsg = extension_settings.chromadb.hhaa_wrapper_msg || defaultSettings.chroma_default_hhaa_wrapper;
wrapperMsg = substituteParams(wrapperMsg, context.name1, context.name2);
if (!wrapperMsg.includes("{{memories}}")) {
wrapperMsg += " {{memories}}";
}
let memoryMsg = extension_settings.chromadb.hhaa_memory_msg || defaultSettings.chroma_default_hhaa_memory;
memoryMsg = substituteParams(memoryMsg, context.name1, context.name2);
if (!memoryMsg.includes("{{message}}")) {
memoryMsg += " {{message}}";
}
// Reversed because we want the most 'important' messages at the bottom.
let recalledMemories = queriedMessages.map(m => m.meta).filter(onlyUnique).map(JSON.parse).reverse();
let tokenApprox = 0;
let allMemoryBlob = "";
let seenMemories = new Set(); // Why are there even duplicates in chromadb anyway?
for (const msg of recalledMemories) {
const memoryBlob = memoryMsg.replace('{{name}}', msg.name).replace('{{message}}', msg.mes);
const memoryTokens = (memoryBlob.length / CHARACTERS_PER_TOKEN_RATIO);
if (!seenMemories.has(memoryBlob) && tokenApprox + memoryTokens <= chromaTokenLimit) {
allMemoryBlob += memoryBlob;
tokenApprox += memoryTokens;
seenMemories.add(memoryBlob);
}
}
// No memories? No prompt.
const promptBlob = (tokenApprox == 0) ? "" : wrapperMsg.replace('{{memories}}', allMemoryBlob);
console.debug("CHROMADB: prompt blob: %o", promptBlob);
context.setExtensionPrompt(MODULE_NAME, promptBlob, extension_prompt_types.IN_PROMPT);
}
if (selectedStrategy === 'custom') {
const context = getContext();
recallMsg = substituteParams(recallMsg, context.name1, context.name2);
if (!recallMsg.includes("{{memories}}")) {
recallMsg += " {{memories}}";
}
let recallStart = recallMsg.split('{{memories}}')[0]
let recallEnd = recallMsg.split('{{memories}}')[1]
newChat.push(
{
is_user: false,
mes: recallStart,
name: "system",
send_date: 0,
}
);
newChat.push(...queriedMessages.map(m => m.meta).filter(onlyUnique).map(JSON.parse));
newChat.push(
{
is_user: false,
mes: recallEnd + `\n`,
name: "system",
send_date: 0,
}
);
//prototype chroma duplicate removal
let chatset = new Set(chat.map(obj => obj.mes));
newChat = newChat.filter(obj => !chatset.has(obj.mes));
if(chromaDepth === -1) {
chat.splice(chat.length, 0, ...newChat);
}
else {
chat.splice(chromaDepth, 0, ...newChat);
}
}
if (selectedStrategy === 'original') {
//removes .length # messages from the start of 'kept messages'
//replaces them with chromaDB results (with no separator)
newChat.push(...queriedMessages.map(m => m.meta).filter(onlyUnique).map(JSON.parse));
chat.splice(0, messagesToStore.length, ...newChat);
}
}
}
function onFreezeInput() {
extension_settings.chromadb.freeze = $('#chromadb_freeze').is(':checked');
saveSettingsDebounced();
}
function onAutoAdjustInput() {
extension_settings.chromadb.auto_adjust = $('#chromadb_auto_adjust').is(':checked');
enableDisableSliders();
saveSettingsDebounced();
}
function onFullLogQuery() {
extension_settings.chromadb.query_last_only = $('#chromadb_query_last_only').is(':checked');
saveSettingsDebounced();
}
function enableDisableSliders() {
const auto_adjust = extension_settings.chromadb.auto_adjust;
$('label[for="chromadb_keep_context"]').prop('hidden', auto_adjust);
$('#chromadb_keep_context').prop('hidden', auto_adjust)
$('label[for="chromadb_n_results"]').prop('hidden', auto_adjust);
$('#chromadb_n_results').prop('hidden', auto_adjust)
$('label[for="chromadb_keep_context_proportion"]').prop('hidden', !auto_adjust);
$('#chromadb_keep_context_proportion').prop('hidden', !auto_adjust)
}
function onKeepContextProportionInput() {
extension_settings.chromadb.keep_context_proportion = $('#chromadb_keep_context_proportion').val();
$('#chromadb_keep_context_proportion_value').text(Math.round(extension_settings.chromadb.keep_context_proportion * 100));
saveSettingsDebounced();
}
jQuery(async () => {
const settingsHtml = `
<div class="chromadb_settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>Smart Context</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<small>This extension rearranges the messages in the current chat to keep more relevant information in the context. Adjust the sliders below based on average amount of messages in your prompt (refer to the chat cut-off line).</small>
<span class="wide100p marginTopBot5 displayBlock">Memory Injection Strategy</span>
<hr>
<select id="chromadb_strategy">
<option value="original">Replace non-kept chat items with memories</option>
<option value="ross">Add memories after chat with a header tag</option>
<option value="hh_aa">Add memory list to character description</option>
<option value="custom">Add memories at custom depth with custom msg</option>
</select>
<label for="chromadb_custom_msg" hidden><small>Custom injection message:</small></label>
<textarea id="chromadb_custom_msg" hidden class="text_pole textarea_compact" rows="2" placeholder="${defaultSettings.chroma_default_msg}" style="height: 61px; display: none;"></textarea>
<label for="chromadb_custom_depth" hidden><small>How deep should the memory messages be injected?: (<span id="chromadb_custom_depth_value"></span>)</small></label>
<input id="chromadb_custom_depth" type="range" min="${defaultSettings.chroma_depth_min}" max="${defaultSettings.chroma_depth_max}" step="${defaultSettings.chroma_depth_step}" value="${defaultSettings.chroma_depth}" hidden/>
<label for="chromadb_hhaa_wrapperfmt" hidden><small>Custom wrapper format:</small></label>
<textarea id="chromadb_hhaa_wrapperfmt" hidden class="text_pole textarea_compact" rows="2" placeholder="${defaultSettings.chroma_default_hhaa_wrapper}" style="height: 61px; display: none;"></textarea>
<label for="chromadb_hhaa_memoryfmt" hidden><small>Custom memory format:</small></label>
<textarea id="chromadb_hhaa_memoryfmt" hidden class="text_pole textarea_compact" rows="2" placeholder="${defaultSettings.chroma_default_hhaa_memory}" style="height: 61px; display: none;"></textarea>
<label for="chromadb_hhaa_token_limit" hidden><small>Maximum tokens allowed for memories: (<span id="chromadb_hhaa_token_limit_value"></span>)</small></label>
<input id="chromadb_hhaa_token_limit" type="range" min="0" max="2048" step="64" value="${defaultSettings.hhaa_token_limit}" hidden/>
<span>Memory Recall Strategy</span>
<select id="chromadb_recall_strategy">
<option value="original">Recall only from this chat</option>
<option value="multichat">Recall from all character chats (experimental)</option>
</select>
<span>Memory Sort Strategy</span>
<select id="chromadb_sort_strategy">
<option value="date">Sort memories by date</option>
<option value="distance">Sort memories by relevance</option>
</select>
<label for="chromadb_keep_context"><small>How many original chat messages to keep: (<span id="chromadb_keep_context_value"></span>) messages</small></label>
<input id="chromadb_keep_context" type="range" min="${defaultSettings.keep_context_min}" max="${defaultSettings.keep_context_max}" step="${defaultSettings.keep_context_step}" value="${defaultSettings.keep_context}" />
<label for="chromadb_n_results"><small>Maximum number of ChromaDB 'memories' to inject: (<span id="chromadb_n_results_value"></span>) messages</small></label>
<input id="chromadb_n_results" type="range" min="${defaultSettings.n_results_min}" max="${defaultSettings.n_results_max}" step="${defaultSettings.n_results_step}" value="${defaultSettings.n_results}" />
<label for="chromadb_keep_context_proportion"><small>Keep (<span id="chromadb_keep_context_proportion_value"></span>%) of in-context chat messages; replace the rest with memories</small></label>
<input id="chromadb_keep_context_proportion" type="range" min="${defaultSettings.keep_context_proportion_min}" max="${defaultSettings.keep_context_proportion_max}" step="${defaultSettings.keep_context_proportion_step}" value="${defaultSettings.keep_context_proportion}" />
<label for="chromadb_split_length"><small>Max length for each 'memory' pulled from the current chat history: (<span id="chromadb_split_length_value"></span>) characters</small></label>
<input id="chromadb_split_length" type="range" min="${defaultSettings.split_length_min}" max="${defaultSettings.split_length_max}" step="${defaultSettings.split_length_step}" value="${defaultSettings.split_length}" />
<label for="chromadb_file_split_length"><small>Max length for each 'memory' pulled from imported text files: (<span id="chromadb_file_split_length_value"></span>) characters</small></label>
<input id="chromadb_file_split_length" type="range" min="${defaultSettings.file_split_length_min}" max="${defaultSettings.file_split_length_max}" step="${defaultSettings.file_split_length_step}" value="${defaultSettings.file_split_length}" />
<label class="checkbox_label" for="chromadb_freeze" title="Pauses the automatic synchronization of new messages with ChromaDB. Older messages and injections will still be pulled as usual." >
<input type="checkbox" id="chromadb_freeze" />
<span>Freeze ChromaDB state</span>
</label>
<label class="checkbox_label for="chromadb_auto_adjust" title="Automatically adjusts the number of messages to keep based on the average number of messages in the current chat and the chosen proportion.">
<input type="checkbox" id="chromadb_auto_adjust" />
<span>Use % strategy</span>
</label>
<label class="checkbox_label" for="chromadb_chunk_nl" title="Chunk injected documents on newline instead of at set character size." >
<input type="checkbox" id="chromadb_chunk_nl" />
<span>Chunk on Newlines</span>
</label>
<label class="checkbox_label for="chromadb_query_last_only" title="ChromaDB queries only use the most recent message. (Instead of using all messages still in the context.)">
<input type="checkbox" id="chromadb_query_last_only" />
<span>Query last message only</span>
</label>
<div class="flex-container spaceEvenly">
<div id="chromadb_inject" title="Upload custom textual data to use in the context of the current chat" class="menu_button">
<i class="fa-solid fa-file-arrow-up"></i>
<span>Inject Data (TXT file)</span>
</div>
<div id="chromadb_export" title="Export all of the current chromadb data for this current chat" class="menu_button">
<i class="fa-solid fa-file-export"></i>
<span>Export</span>
</div>
<div id="chromadb_import" title="Import a full chromadb export for this current chat" class="menu_button">
<i class="fa-solid fa-file-import"></i>
<span>Import</span>
</div>
<div id="chromadb_purge" title="Force purge all the data related to the current chat from the database" class="menu_button">
<i class="fa-solid fa-broom"></i>
<span>Purge Chat from the DB</span>
</div>
</div>
<small><i>Local ChromaDB now persists to disk by default. The default folder is .chroma_db, and you can set a different folder with the --chroma-folder argument. If you are using the Extras Colab notebook, you will need to inject the text data every time the Extras API server is restarted.</i></small>
</div>
<form><input id="chromadb_inject_file" type="file" accept="text/plain" hidden></form>
<form><input id="chromadb_import_file" type="file" accept="application/json" hidden></form>
</div>`;
$('#extensions_settings2').append(settingsHtml);
$('#chromadb_strategy').on('change', onStrategyChange);
$('#chromadb_recall_strategy').on('change', onRecallStrategyChange);
$('#chromadb_sort_strategy').on('change', onSortStrategyChange);
$('#chromadb_keep_context').on('input', onKeepContextInput);
$('#chromadb_n_results').on('input', onNResultsInput);
$('#chromadb_custom_depth').on('input', onChromaDepthInput);
$('#chromadb_custom_msg').on('input', onChromaMsgInput);
$('#chromadb_hhaa_wrapperfmt').on('input', onChromaHHAAWrapper);
$('#chromadb_hhaa_memoryfmt').on('input', onChromaHHAAMemory);
$('#chromadb_hhaa_token_limit').on('input', onChromaHHAATokens);
$('#chromadb_split_length').on('input', onSplitLengthInput);
$('#chromadb_file_split_length').on('input', onFileSplitLengthInput);
$('#chromadb_inject').on('click', () => $('#chromadb_inject_file').trigger('click'));
$('#chromadb_import').on('click', () => $('#chromadb_import_file').trigger('click'));
$('#chromadb_inject_file').on('change', onSelectInjectFile);
$('#chromadb_import_file').on('change', onSelectImportFile);
$('#chromadb_purge').on('click', onPurgeClick);
$('#chromadb_export').on('click', onExportClick);
$('#chromadb_freeze').on('input', onFreezeInput);
$('#chromadb_chunk_nl').on('input', onChunkNLInput);
$('#chromadb_auto_adjust').on('input', onAutoAdjustInput);
$('#chromadb_query_last_only').on('input', onFullLogQuery);
$('#chromadb_keep_context_proportion').on('input', onKeepContextProportionInput);
await loadSettings();
// Not sure if this is needed, but it's here just in case
eventSource.on(event_types.MESSAGE_DELETED, getChatSyncState);
eventSource.on(event_types.MESSAGE_RECEIVED, getChatSyncState);
eventSource.on(event_types.MESSAGE_SENT, getChatSyncState);
// Will make the sync state update when a message is edited or swiped
eventSource.on(event_types.MESSAGE_EDITED, invalidateMessageSyncState);
eventSource.on(event_types.MESSAGE_SWIPED, invalidateMessageSyncState);
});

View File

@@ -1,14 +0,0 @@
{
"display_name": "Smart Context",
"loading_order": 11,
"requires": [
"chromadb"
],
"optional": [],
"generate_interceptor": "chromadb_interceptGeneration",
"js": "index.js",
"css": "style.css",
"author": "maceter636@proton.me",
"version": "1.0.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
}

View File

@@ -1,7 +0,0 @@
.chromadb_settings .menu_button {
width: fit-content;
display: flex;
gap: 10px;
align-items: center;
flex-direction: row;
}

View File

@@ -3,6 +3,8 @@ import { getContext, getApiUrl, extension_settings, doExtrasFetch, modules } fro
import { eventSource, event_types, extension_prompt_types, generateQuietPrompt, is_send_press, saveSettingsDebounced, substituteParams } from "../../../script.js";
import { is_group_generating, selected_group } from "../../group-chats.js";
import { registerSlashCommand } from "../../slash-commands.js";
import { loadMovingUIState } from '../../power-user.js';
import { dragElement } from "../../RossAscends-mods.js";
export { MODULE_NAME };
const MODULE_NAME = '1_memory';
@@ -61,6 +63,7 @@ const defaultSettings = {
maxLengthPenalty: 4,
lengthPenaltyStep: 0.1,
memoryFrozen: false,
SkipWIAN: false,
source: summary_sources.extras,
prompt: defaultPrompt,
template: defaultTemplate,
@@ -98,6 +101,7 @@ function loadSettings() {
$('#memory_temperature').val(extension_settings.memory.temperature).trigger('input');
$('#memory_length_penalty').val(extension_settings.memory.lengthPenalty).trigger('input');
$('#memory_frozen').prop('checked', extension_settings.memory.memoryFrozen).trigger('input');
$('#memory_skipWIAN').prop('checked', extension_settings.memory.SkipWIAN).trigger('input');
$('#memory_prompt').val(extension_settings.memory.prompt).trigger('input');
$('#memory_prompt_words').val(extension_settings.memory.promptWords).trigger('input');
$('#memory_prompt_interval').val(extension_settings.memory.promptInterval).trigger('input');
@@ -168,6 +172,12 @@ function onMemoryFrozenInput() {
saveSettingsDebounced();
}
function onMemorySkipWIANInput() {
const value = Boolean($(this).prop('checked'));
extension_settings.memory.SkipWIAN = value;
saveSettingsDebounced();
}
function onMemoryPromptWordsInput() {
const value = $(this).val();
extension_settings.memory.promptWords = Number(value);
@@ -309,13 +319,15 @@ async function onChatEvent() {
async function forceSummarizeChat() {
const context = getContext();
const skipWIAN = extension_settings.memory.SkipWIAN
console.log(`Skipping WIAN? ${skipWIAN}`)
if (!context.chatId) {
toastr.warning('No chat selected');
return;
}
toastr.info('Summarizing chat...', 'Please wait');
const value = await summarizeChatMain(context, true);
const value = await summarizeChatMain(context, true, skipWIAN);
if (!value) {
toastr.warning('Failed to summarize chat');
@@ -324,19 +336,21 @@ async function forceSummarizeChat() {
}
async function summarizeChat(context) {
const skipWIAN = extension_settings.memory.SkipWIAN
switch (extension_settings.memory.source) {
case summary_sources.extras:
await summarizeChatExtras(context);
break;
case summary_sources.main:
await summarizeChatMain(context, false);
await summarizeChatMain(context, false, skipWIAN);
break;
default:
break;
}
}
async function summarizeChatMain(context, force) {
async function summarizeChatMain(context, force, skipWIAN) {
if (extension_settings.memory.promptInterval === 0 && !force) {
console.debug('Prompt interval is set to 0, skipping summarization');
return;
@@ -395,8 +409,8 @@ async function summarizeChatMain(context, force) {
console.debug('Summarization prompt is empty. Skipping summarization.');
return;
}
const summary = await generateQuietPrompt(prompt, false);
console.log('sending summary prompt')
const summary = await generateQuietPrompt(prompt, false, skipWIAN);
const newContext = getContext();
// something changed during summarization request
@@ -547,100 +561,178 @@ function setMemoryContext(value, saveToMessage) {
}
}
function doPopout(e) {
const target = e.target;
//repurposes the zoomed avatar template to server as a floating div
if ($("#summaryExtensionPopout").length === 0) {
console.debug('did not see popout yet, creating')
const originalHTMLClone = $(target).parent().parent().parent().find('.inline-drawer-content').html()
const originalElement = $(target).parent().parent().parent().find('.inline-drawer-content')
const template = $('#zoomed_avatar_template').html();
const controlBarHtml = `<div class="panelControlBar flex-container">
<div id="summaryExtensionPopoutheader" class="fa-solid fa-grip drag-grabber hoverglow"></div>
<div id="summaryExtensionPopoutClose" class="fa-solid fa-circle-xmark hoverglow dragClose"></div>
</div>`
const newElement = $(template);
newElement.attr('id', 'summaryExtensionPopout')
.removeClass('zoomed_avatar')
.addClass('draggable')
.empty()
const prevSummaryBoxContents = $('#memory_contents').val(); //copy summary box before emptying
originalElement.empty();
originalElement.html(`<div class="flex-container alignitemscenter justifyCenter wide100p"><small>Currently popped out</small></div>`)
newElement.append(controlBarHtml).append(originalHTMLClone)
$('body').append(newElement);
$("#summaryExtensionDrawerContents").addClass('scrollableInnerFull')
setMemoryContext(prevSummaryBoxContents, false); //paste prev summary box contents into popout box
setupListeners();
loadSettings();
loadMovingUIState();
$("#summaryExtensionPopout").fadeIn(250);
dragElement(newElement);
//setup listener for close button to restore extensions menu
$('#summaryExtensionPopoutClose').off('click').on('click', function () {
$("#summaryExtensionDrawerContents").removeClass('scrollableInnerFull')
const summaryPopoutHTML = $("#summaryExtensionDrawerContents")
$("#summaryExtensionPopout").fadeOut(250, () => {
originalElement.empty();
originalElement.html(summaryPopoutHTML);
$("#summaryExtensionPopout").remove()
})
loadSettings();
})
} else {
console.debug('saw existing popout, removing')
$("#summaryExtensionPopout").fadeOut(250, () => { $("#summaryExtensionPopoutClose").trigger('click') });
}
}
function setupListeners() {
//setup shared listeners for popout and regular ext menu
$('#memory_restore').off('click').on('click', onMemoryRestoreClick);
$('#memory_contents').off('click').on('input', onMemoryContentInput);
$('#memory_long_length').off('click').on('input', onMemoryLongInput);
$('#memory_short_length').off('click').on('input', onMemoryShortInput);
$('#memory_repetition_penalty').off('click').on('input', onMemoryRepetitionPenaltyInput);
$('#memory_temperature').off('click').on('input', onMemoryTemperatureInput);
$('#memory_length_penalty').off('click').on('input', onMemoryLengthPenaltyInput);
$('#memory_frozen').off('click').on('input', onMemoryFrozenInput);
$('#memory_skipWIAN').off('click').on('input', onMemorySkipWIANInput);
$('#summary_source').off('click').on('change', onSummarySourceChange);
$('#memory_prompt_words').off('click').on('input', onMemoryPromptWordsInput);
$('#memory_prompt_interval').off('click').on('input', onMemoryPromptIntervalInput);
$('#memory_prompt').off('click').on('input', onMemoryPromptInput);
$('#memory_force_summarize').off('click').on('click', forceSummarizeChat);
$('#memory_template').off('click').on('input', onMemoryTemplateInput);
$('#memory_depth').off('click').on('input', onMemoryDepthInput);
$('input[name="memory_position"]').off('click').on('change', onMemoryPositionChange);
$('#memory_prompt_words_force').off('click').on('input', onMemoryPromptWordsForceInput);
$("#summarySettingsBlockToggle").off('click').on('click', function () {
console.log('saw settings button click')
$("#summarySettingsBlock").slideToggle(200, "swing"); //toggleClass("hidden");
});
}
jQuery(function () {
function addExtensionControls() {
const settingsHtml = `
<div id="memory_settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>Summarize</b>
<div class="flex-container alignitemscenter margin0"><b>Summarize</b><i id="summaryExtensionPopoutButton" class="fa-solid fa-window-restore menu_button margin0"></i></div>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<label for="summary_source">Summarization source:</label>
<select id="summary_source">
<option value="main">Main API</option>
<option value="extras">Extras API</option>
</select>
<label for="memory_contents">Current summary: </label>
<textarea id="memory_contents" class="text_pole textarea_compact" rows="6" placeholder="Summary will be generated here..."></textarea>
<div class="memory_contents_controls">
<input id="memory_restore" class="menu_button" type="button" value="Restore previous state" />
<label for="memory_frozen"><input id="memory_frozen" type="checkbox" />Pause summarization</label>
</div>
<div class="memory_template">
<label for="memory_template">Injection template:</label>
<textarea id="memory_template" class="text_pole textarea_compact" rows="1" placeholder="Use {{summary}} macro to specify the position of summarized text."></textarea>
</div>
<label for="memory_position">Injection position:</label>
<div class="radio_group">
<label>
<input type="radio" name="memory_position" value="2" />
Before Main Prompt / Story String
</label>
<label>
<input type="radio" name="memory_position" value="0" />
After Main Prompt / Story String
</label>
<label>
<input type="radio" name="memory_position" value="1" />
In-chat @ Depth <input id="memory_depth" class="text_pole widthUnset" type="number" min="0" max="999" />
</label>
</div>
<div data-source="main" class="memory_contents_controls">
</div>
<div data-source="main">
<label for="memory_prompt" class="title_restorable">
Summarization Prompt
<div id="summaryExtensionDrawerContents">
<label for="summary_source">Summarize with:</label>
<select id="summary_source">
<option value="main">Main API</option>
<option value="extras">Extras API</option>
</select><br>
<div class="flex-container justifyspacebetween alignitemscenter">
<span class="flex1">Current summary:</span>
<div id="memory_restore" class="menu_button flex1 margin0"><span>Restore Previous</span></div>
</div>
<textarea id="memory_contents" class="text_pole textarea_compact" rows="6" placeholder="Summary will be generated here..."></textarea>
<div class="memory_contents_controls">
<div id="memory_force_summarize" class="menu_button menu_button_icon">
<i class="fa-solid fa-database"></i>
<span>Generate now</span>
<span>Summarize now</span>
</div>
</label>
<textarea id="memory_prompt" class="text_pole textarea_compact" rows="6" placeholder="This prompt will be used in summary generation. Insert {{words}} macro to use the "Number of words" parameter."></textarea>
<label for="memory_prompt_words">Number of words in the summary (<span id="memory_prompt_words_value"></span> words)</label>
<input id="memory_prompt_words" type="range" value="${defaultSettings.promptWords}" min="${defaultSettings.promptMinWords}" max="${defaultSettings.promptMaxWords}" step="${defaultSettings.promptWordsStep}" />
<label for="memory_prompt_interval">Update interval (<span id="memory_prompt_interval_value"></span> messages)</label>
<small>Set to 0 to disable</small>
<input id="memory_prompt_interval" type="range" value="${defaultSettings.promptInterval}" min="${defaultSettings.promptMinInterval}" max="${defaultSettings.promptMaxInterval}" step="${defaultSettings.promptIntervalStep}" />
<label for="memory_prompt_words_force">Force update after (<span id="memory_prompt_words_force_value"></span> words)</label>
<small>Set to 0 to disable</small>
<input id="memory_prompt_words_force" type="range" value="${defaultSettings.promptForceWords}" min="${defaultSettings.promptMinForceWords}" max="${defaultSettings.promptMaxForceWords}" step="${defaultSettings.promptForceWordsStep}" />
</div>
<div data-source="extras">
<label for="memory_short_length">Chat to Summarize buffer length (<span id="memory_short_length_tokens"></span> tokens)</label>
<input id="memory_short_length" type="range" value="${defaultSettings.shortMemoryLength}" min="${defaultSettings.minShortMemory}" max="${defaultSettings.maxShortMemory}" step="${defaultSettings.shortMemoryStep}" />
<label for="memory_long_length">Summary output length (<span id="memory_long_length_tokens"></span> tokens)</label>
<input id="memory_long_length" type="range" value="${defaultSettings.longMemoryLength}" min="${defaultSettings.minLongMemory}" max="${defaultSettings.maxLongMemory}" step="${defaultSettings.longMemoryStep}" />
<label for="memory_temperature">Temperature (<span id="memory_temperature_value"></span>)</label>
<input id="memory_temperature" type="range" value="${defaultSettings.temperature}" min="${defaultSettings.minTemperature}" max="${defaultSettings.maxTemperature}" step="${defaultSettings.temperatureStep}" />
<label for="memory_repetition_penalty">Repetition penalty (<span id="memory_repetition_penalty_value"></span>)</label>
<input id="memory_repetition_penalty" type="range" value="${defaultSettings.repetitionPenalty}" min="${defaultSettings.minRepetitionPenalty}" max="${defaultSettings.maxRepetitionPenalty}" step="${defaultSettings.repetitionPenaltyStep}" />
<label for="memory_length_penalty">Length preference <small>[higher = longer summaries]</small> (<span id="memory_length_penalty_value"></span>)</label>
<input id="memory_length_penalty" type="range" value="${defaultSettings.lengthPenalty}" min="${defaultSettings.minLengthPenalty}" max="${defaultSettings.maxLengthPenalty}" step="${defaultSettings.lengthPenaltyStep}" />
<label for="memory_frozen"><input id="memory_frozen" type="checkbox" />Pause</label>
<label for="memory_skipWIAN"><input id="memory_skipWIAN" type="checkbox" />No WI/AN</label>
</div>
<div class="memory_contents_controls">
<div id="summarySettingsBlockToggle" class="menu_button">Settings</div>
</div>
<div id="summarySettingsBlock" style="display:none;">
<div class="memory_template">
<label for="memory_template">Insertion string:</label>
<textarea id="memory_template" class="text_pole textarea_compact" rows="2" placeholder="{{summary}} will resolve to the current summary contents."></textarea>
</div>
<label for="memory_position">Position:</label>
<div class="radio_group">
<label>
<input type="radio" name="memory_position" value="2" />
Before Main Prompt / Story String
</label>
<label>
<input type="radio" name="memory_position" value="0" />
After Main Prompt / Story String
</label>
<label>
<input type="radio" name="memory_position" value="1" />
In-chat @ Depth <input id="memory_depth" class="text_pole widthUnset" type="number" min="0" max="999" />
</label>
</div>
<div data-source="main" class="memory_contents_controls">
</div>
<div data-source="main">
<label for="memory_prompt" class="title_restorable">
Summary Prompt
</label>
<textarea id="memory_prompt" class="text_pole textarea_compact" rows="6" placeholder="This prompt will be sent to AI to request the summary generation. {{words}} will resolve to the 'Number of words' parameter."></textarea>
<label for="memory_prompt_words">Summary length (<span id="memory_prompt_words_value"></span> words)</label>
<input id="memory_prompt_words" type="range" value="${defaultSettings.promptWords}" min="${defaultSettings.promptMinWords}" max="${defaultSettings.promptMaxWords}" step="${defaultSettings.promptWordsStep}" />
<label for="memory_prompt_interval">Update every <span id="memory_prompt_interval_value"></span> messages</label>
<small>0 = disable</small>
<input id="memory_prompt_interval" type="range" value="${defaultSettings.promptInterval}" min="${defaultSettings.promptMinInterval}" max="${defaultSettings.promptMaxInterval}" step="${defaultSettings.promptIntervalStep}" />
<label for="memory_prompt_words_force">Update every <span id="memory_prompt_words_force_value"></span> words</label>
<small>0 = disable</small>
<input id="memory_prompt_words_force" type="range" value="${defaultSettings.promptForceWords}" min="${defaultSettings.promptMinForceWords}" max="${defaultSettings.promptMaxForceWords}" step="${defaultSettings.promptForceWordsStep}" />
<small>If both sliders are non-zero, then both will trigger summary updates a their respective intervals.</small>
</div>
<div data-source="extras">
<label for="memory_short_length">Chat to Summarize buffer length (<span id="memory_short_length_tokens"></span> tokens)</label>
<input id="memory_short_length" type="range" value="${defaultSettings.shortMemoryLength}" min="${defaultSettings.minShortMemory}" max="${defaultSettings.maxShortMemory}" step="${defaultSettings.shortMemoryStep}" />
<label for="memory_long_length">Summary output length (<span id="memory_long_length_tokens"></span> tokens)</label>
<input id="memory_long_length" type="range" value="${defaultSettings.longMemoryLength}" min="${defaultSettings.minLongMemory}" max="${defaultSettings.maxLongMemory}" step="${defaultSettings.longMemoryStep}" />
<label for="memory_temperature">Temperature (<span id="memory_temperature_value"></span>)</label>
<input id="memory_temperature" type="range" value="${defaultSettings.temperature}" min="${defaultSettings.minTemperature}" max="${defaultSettings.maxTemperature}" step="${defaultSettings.temperatureStep}" />
<label for="memory_repetition_penalty">Repetition penalty (<span id="memory_repetition_penalty_value"></span>)</label>
<input id="memory_repetition_penalty" type="range" value="${defaultSettings.repetitionPenalty}" min="${defaultSettings.minRepetitionPenalty}" max="${defaultSettings.maxRepetitionPenalty}" step="${defaultSettings.repetitionPenaltyStep}" />
<label for="memory_length_penalty">Length preference <small>[higher = longer summaries]</small> (<span id="memory_length_penalty_value"></span>)</label>
<input id="memory_length_penalty" type="range" value="${defaultSettings.lengthPenalty}" min="${defaultSettings.minLengthPenalty}" max="${defaultSettings.maxLengthPenalty}" step="${defaultSettings.lengthPenaltyStep}" />
</div>
</div>
</div>
</div>
</div>
</div>
`;
$('#extensions_settings2').append(settingsHtml);
$('#memory_restore').on('click', onMemoryRestoreClick);
$('#memory_contents').on('input', onMemoryContentInput);
$('#memory_long_length').on('input', onMemoryLongInput);
$('#memory_short_length').on('input', onMemoryShortInput);
$('#memory_repetition_penalty').on('input', onMemoryRepetitionPenaltyInput);
$('#memory_temperature').on('input', onMemoryTemperatureInput);
$('#memory_length_penalty').on('input', onMemoryLengthPenaltyInput);
$('#memory_frozen').on('input', onMemoryFrozenInput);
$('#summary_source').on('change', onSummarySourceChange);
$('#memory_prompt_words').on('input', onMemoryPromptWordsInput);
$('#memory_prompt_interval').on('input', onMemoryPromptIntervalInput);
$('#memory_prompt').on('input', onMemoryPromptInput);
$('#memory_force_summarize').on('click', forceSummarizeChat);
$('#memory_template').on('input', onMemoryTemplateInput);
$('#memory_depth').on('input', onMemoryDepthInput);
$('input[name="memory_position"]').on('change', onMemoryPositionChange);
$('#memory_prompt_words_force').on('input', onMemoryPromptWordsForceInput);
setupListeners();
$("#summaryExtensionPopoutButton").off('click').on('click', function (e) {
doPopout(e);
e.stopPropagation();
});
}
addExtensionControls();

View File

@@ -1,5 +1,5 @@
{
"display_name": "Memory",
"display_name": "Summarize",
"loading_order": 9,
"requires": [],
"optional": [

View File

@@ -8,7 +8,8 @@
line-height: 1.2;
}
label[for="memory_frozen"] {
label[for="memory_frozen"],
label[for="memory_skipWIAN"] {
display: flex;
align-items: center;
margin: 0 !important;
@@ -23,4 +24,4 @@ label[for="memory_frozen"] input {
flex-direction: row;
align-items: center;
justify-content: space-between;
}
}

View File

@@ -1,811 +0,0 @@
import { chat_metadata, callPopup, saveSettingsDebounced, is_send_press } from "../../../script.js";
import { getContext, extension_settings, saveMetadataDebounced } from "../../extensions.js";
import {
substituteParams,
eventSource,
event_types,
generateQuietPrompt,
} from "../../../script.js";
import { registerSlashCommand } from "../../slash-commands.js";
import { waitUntilCondition } from "../../utils.js";
import { is_group_generating, selected_group } from "../../group-chats.js";
const MODULE_NAME = "Objective"
let taskTree = null
let globalTasks = []
let currentChatId = ""
let currentObjective = null
let currentTask = null
let checkCounter = 0
let lastMessageWasSwipe = false
const defaultPrompts = {
"createTask": `Pause your roleplay. Please generate a numbered list of plain text tasks to complete an objective. The objective that you must make a numbered task list for is: "{{objective}}". The tasks created should take into account the character traits of {{char}}. These tasks may or may not involve {{user}} directly. Include the objective as the final task.`,
"checkTaskCompleted": `Pause your roleplay. Determine if this task is completed: [{{task}}]. To do this, examine the most recent messages. Your response must only contain either true or false, and nothing else. Example output: true`,
'currentTask':`Your current task is [{{task}}]. Balance existing roleplay with completing this task.`,
}
let objectivePrompts = defaultPrompts
//###############################//
//# Task Management #//
//###############################//
// Return the task and index or throw an error
function getTaskById(taskId){
if (taskId == null) {
throw `Null task id`
}
return getTaskByIdRecurse(taskId, taskTree)
}
function getTaskByIdRecurse(taskId, task) {
if (task.id == taskId){
return task
}
for (const childTask of task.children) {
const foundTask = getTaskByIdRecurse(taskId, childTask);
if (foundTask != null) {
return foundTask;
}
}
return null;
}
function substituteParamsPrompts(content, substituteGlobal) {
content = content.replace(/{{objective}}/gi, currentObjective.description)
content = content.replace(/{{task}}/gi, currentTask.description)
if (currentTask.parent){
content = content.replace(/{{parent}}/gi, currentTask.parent.description)
}
if (substituteGlobal) {
content = substituteParams(content)
}
return content
}
// Call Quiet Generate to create task list using character context, then convert to tasks. Should not be called much.
async function generateTasks() {
const prompt = substituteParamsPrompts(objectivePrompts.createTask, false);
console.log(`Generating tasks for objective with prompt`)
toastr.info('Generating tasks for objective', 'Please wait...');
const taskResponse = await generateQuietPrompt(prompt)
// Clear all existing objective tasks when generating
currentObjective.children = []
const numberedListPattern = /^\d+\./
// Create tasks from generated task list
for (const task of taskResponse.split('\n').map(x => x.trim())) {
if (task.match(numberedListPattern) != null) {
currentObjective.addTask(task.replace(numberedListPattern,"").trim())
}
}
updateUiTaskList();
setCurrentTask();
console.info(`Response for Objective: '${currentObjective.description}' was \n'${taskResponse}', \nwhich created tasks \n${JSON.stringify(currentObjective.children.map(v => {return v.toSaveState()}), null, 2)} `)
toastr.success(`Generated ${currentObjective.children.length} tasks`, 'Done!');
}
// Call Quiet Generate to check if a task is completed
async function checkTaskCompleted() {
// Make sure there are tasks
if (jQuery.isEmptyObject(currentTask)) {
return
}
try {
// Wait for group to finish generating
if (selected_group) {
await waitUntilCondition(() => is_group_generating === false, 1000, 10);
}
// Another extension might be doing something with the chat, so wait for it to finish
await waitUntilCondition(() => is_send_press === false, 30000, 10);
} catch {
console.debug("Failed to wait for group to finish generating")
return;
}
checkCounter = $('#objective-check-frequency').val()
toastr.info("Checking for task completion.")
const prompt = substituteParamsPrompts(objectivePrompts.checkTaskCompleted, false);
const taskResponse = (await generateQuietPrompt(prompt)).toLowerCase()
// Check response if task complete
if (taskResponse.includes("true")) {
console.info(`Character determined task '${currentTask.description} is completed.`)
currentTask.completeTask()
} else if (!(taskResponse.includes("false"))) {
console.warn(`checkTaskCompleted response did not contain true or false. taskResponse: ${taskResponse}`)
} else {
console.debug(`Checked task completion. taskResponse: ${taskResponse}`)
}
}
function getNextIncompleteTaskRecurse(task){
if (task.completed === false // Return task if incomplete
&& task.children.length === 0 // Ensure task has no children, it's subtasks will determine completeness
&& task.parentId !== "" // Must have parent id. Only root task will be missing this and we dont want that
){
return task
}
for (const childTask of task.children) {
if (childTask.completed === true){ // Don't recurse into completed tasks
continue
}
const foundTask = getNextIncompleteTaskRecurse(childTask);
if (foundTask != null) {
return foundTask;
}
}
return null;
}
// Set a task in extensionPrompt context. Defaults to first incomplete
function setCurrentTask(taskId = null, skipSave = false) {
const context = getContext();
// TODO: Should probably null this rather than set empty object
currentTask = {};
// Find the task, either next incomplete, or by provided taskId
if (taskId === null) {
currentTask = getNextIncompleteTaskRecurse(taskTree) || {};
} else {
currentTask = getTaskById(taskId);
}
// Don't just check for a current task, check if it has data
const description = currentTask.description || null;
if (description) {
const extensionPromptText = substituteParamsPrompts(objectivePrompts.currentTask, true);
// Remove highlights
$('.objective-task').css({'border-color':'','border-width':''})
// Highlight current task
let highlightTask = currentTask
while (highlightTask.parentId !== ""){
if (highlightTask.descriptionSpan){
highlightTask.descriptionSpan.css({'border-color':'yellow','border-width':'2px'});
}
const parent = getTaskById(highlightTask.parentId)
highlightTask = parent
}
// Update the extension prompt
context.setExtensionPrompt(MODULE_NAME, extensionPromptText, 1, $('#objective-chat-depth').val());
console.info(`Current task in context.extensionPrompts.Objective is ${JSON.stringify(context.extensionPrompts.Objective)}`);
} else {
context.setExtensionPrompt(MODULE_NAME, '');
console.info(`No current task`);
}
// Save state if not skipping
if (!skipSave) {
saveState();
}
}
function getHighestTaskIdRecurse(task) {
let nextId = task.id;
for (const childTask of task.children) {
const childId = getHighestTaskIdRecurse(childTask);
if (childId > nextId) {
nextId = childId;
}
}
return nextId;
}
//###############################//
//# Task Class #//
//###############################//
class ObjectiveTask {
id
description
completed
parentId
children
// UI Elements
taskHtml
descriptionSpan
completedCheckbox
deleteTaskButton
addTaskButton
constructor ({id=undefined, description, completed=false, parentId=""}) {
this.description = description
this.parentId = parentId
this.children = []
this.completed = completed
// Generate a new ID if none specified
if (id==undefined){
this.id = getHighestTaskIdRecurse(taskTree) + 1
} else {
this.id=id
}
}
// Accepts optional index. Defaults to adding to end of list.
addTask(description, index = null) {
index = index != null ? index: index = this.children.length
this.children.splice(index, 0, new ObjectiveTask(
{description: description, parentId: this.id}
))
saveState()
}
getIndex(){
if (this.parentId !== null) {
const parent = getTaskById(this.parentId)
const index = parent.children.findIndex(task => task.id === this.id)
if (index === -1){
throw `getIndex failed: Task '${this.description}' not found in parent task '${parent.description}'`
}
return index
} else {
throw `getIndex failed: Task '${this.description}' has no parent`
}
}
// Used to set parent to complete when all child tasks are completed
checkParentComplete() {
let all_completed = true;
if (this.parentId !== ""){
const parent = getTaskById(this.parentId);
for (const child of parent.children){
if (!child.completed){
all_completed = false;
break;
}
}
if (all_completed){
parent.completed = true;
console.info(`Parent task '${parent.description}' completed after all child tasks complated.`)
} else {
parent.completed = false;
}
}
}
// Complete the current task, setting next task to next incomplete task
completeTask() {
this.completed = true
console.info(`Task successfully completed: ${JSON.stringify(this.description)}`)
this.checkParentComplete()
setCurrentTask()
updateUiTaskList()
}
// Add a single task to the UI and attach event listeners for user edits
addUiElement() {
const template = `
<div id="objective-task-label-${this.id}" class="flex1 checkbox_label">
<input id="objective-task-complete-${this.id}" type="checkbox">
<span class="text_pole objective-task" style="display: block" id="objective-task-description-${this.id}" contenteditable>${this.description}</span>
<div id="objective-task-delete-${this.id}" class="objective-task-button fa-solid fa-xmark fa-2x" title="Delete Task"></div>
<div id="objective-task-add-${this.id}" class="objective-task-button fa-solid fa-plus fa-2x" title="Add Task"></div>
<div id="objective-task-add-branch-${this.id}" class="objective-task-button fa-solid fa-code-fork fa-2x" title="Branch Task"></div>
</div><br>
`;
// Add the filled out template
$('#objective-tasks').append(template);
this.completedCheckbox = $(`#objective-task-complete-${this.id}`);
this.descriptionSpan = $(`#objective-task-description-${this.id}`);
this.addButton = $(`#objective-task-add-${this.id}`);
this.deleteButton = $(`#objective-task-delete-${this.id}`);
this.taskHtml = $(`#objective-task-label-${this.id}`);
this.branchButton = $(`#objective-task-add-branch-${this.id}`)
// Handle sub-task forking style
if (this.children.length > 0){
this.branchButton.css({'color':'#33cc33'})
} else {
this.branchButton.css({'color':''})
}
// Add event listeners and set properties
$(`#objective-task-complete-${this.id}`).prop('checked', this.completed);
$(`#objective-task-complete-${this.id}`).on('click', () => (this.onCompleteClick()));
$(`#objective-task-description-${this.id}`).on('keyup', () => (this.onDescriptionUpdate()));
$(`#objective-task-description-${this.id}`).on('focusout', () => (this.onDescriptionFocusout()));
$(`#objective-task-delete-${this.id}`).on('click', () => (this.onDeleteClick()));
$(`#objective-task-add-${this.id}`).on('click', () => (this.onAddClick()));
this.branchButton.on('click', () => (this.onBranchClick()))
}
onBranchClick() {
currentObjective = this
updateUiTaskList();
setCurrentTask();
}
onCompleteClick(){
this.completed = this.completedCheckbox.prop('checked')
this.checkParentComplete()
setCurrentTask();
}
onDescriptionUpdate(){
this.description = this.descriptionSpan.text();
}
onDescriptionFocusout(){
setCurrentTask();
}
onDeleteClick(){
const index = this.getIndex()
const parent = getTaskById(this.parentId)
parent.children.splice(index, 1)
updateUiTaskList()
setCurrentTask()
}
onAddClick(){
const index = this.getIndex()
const parent = getTaskById(this.parentId)
parent.addTask("", index + 1);
updateUiTaskList();
setCurrentTask();
}
toSaveStateRecurse() {
let children = []
if (this.children.length > 0){
for (const child of this.children){
children.push(child.toSaveStateRecurse())
}
}
return {
"id":this.id,
"description":this.description,
"completed":this.completed,
"parentId": this.parentId,
"children": children,
}
}
}
//###############################//
//# Custom Prompts #//
//###############################//
function onEditPromptClick() {
let popupText = ''
popupText += `
<div class="objective_prompt_modal">
<small>Edit prompts used by Objective for this session. You can use {{objective}} or {{task}} plus any other standard template variables. Save template to persist changes.</small>
<br>
<div>
<label for="objective-prompt-generate">Generation Prompt</label>
<textarea id="objective-prompt-generate" type="text" class="text_pole textarea_compact" rows="8"></textarea>
<label for="objective-prompt-check">Completion Check Prompt</label>
<textarea id="objective-prompt-check" type="text" class="text_pole textarea_compact" rows="8"></textarea>
<label for="objective-prompt-extension-prompt">Injected Prompt</label>
<textarea id="objective-prompt-extension-prompt" type="text" class="text_pole textarea_compact" rows="8"></textarea>
</div>
<div class="objective_prompt_block">
<label for="objective-custom-prompt-select">Custom Prompt Select</label>
<select id="objective-custom-prompt-select"><select>
</div>
<div class="objective_prompt_block">
<input id="objective-custom-prompt-new" class="menu_button" type="submit" value="New Prompt" />
<input id="objective-custom-prompt-save" class="menu_button" type="submit" value="Save Prompt" />
<input id="objective-custom-prompt-delete" class="menu_button" type="submit" value="Delete Prompt" />
</div>
</div>`
callPopup(popupText, 'text')
populateCustomPrompts()
// Set current values
$('#objective-prompt-generate').val(objectivePrompts.createTask)
$('#objective-prompt-check').val(objectivePrompts.checkTaskCompleted)
$('#objective-prompt-extension-prompt').val(objectivePrompts.currentTask)
// Handle value updates
$('#objective-prompt-generate').on('input', () => {
objectivePrompts.createTask = $('#objective-prompt-generate').val()
})
$('#objective-prompt-check').on('input', () => {
objectivePrompts.checkTaskCompleted = $('#objective-prompt-check').val()
})
$('#objective-prompt-extension-prompt').on('input', () => {
objectivePrompts.currentTask = $('#objective-prompt-extension-prompt').val()
})
// Handle new
$('#objective-custom-prompt-new').on('click', () => {
newCustomPrompt()
})
// Handle save
$('#objective-custom-prompt-save').on('click', () => {
saveCustomPrompt()
})
// Handle delete
$('#objective-custom-prompt-delete').on('click', () => {
deleteCustomPrompt()
})
// Handle load
$('#objective-custom-prompt-select').on('change', loadCustomPrompt)
}
async function newCustomPrompt() {
const customPromptName = await callPopup('<h3>Custom Prompt name:</h3>', 'input');
if (customPromptName == "") {
toastr.warning("Please set custom prompt name to save.")
return
}
if (customPromptName == "default"){
toastr.error("Cannot save over default prompt")
return
}
extension_settings.objective.customPrompts[customPromptName] = {}
Object.assign(extension_settings.objective.customPrompts[customPromptName], objectivePrompts)
saveSettingsDebounced()
populateCustomPrompts()
}
function saveCustomPrompt() {
const customPromptName = $("#objective-custom-prompt-select").find(':selected').val()
if (customPromptName == "default"){
toastr.error("Cannot save over default prompt")
return
}
Object.assign(extension_settings.objective.customPrompts[customPromptName], objectivePrompts)
saveSettingsDebounced()
populateCustomPrompts()
}
function deleteCustomPrompt(){
const customPromptName = $("#objective-custom-prompt-select").find(':selected').val()
if (customPromptName == "default"){
toastr.error("Cannot delete default prompt")
return
}
delete extension_settings.objective.customPrompts[customPromptName]
saveSettingsDebounced()
populateCustomPrompts()
loadCustomPrompt()
}
function loadCustomPrompt(){
const optionSelected = $("#objective-custom-prompt-select").find(':selected').val()
Object.assign(objectivePrompts, extension_settings.objective.customPrompts[optionSelected])
$('#objective-prompt-generate').val(objectivePrompts.createTask)
$('#objective-prompt-check').val(objectivePrompts.checkTaskCompleted)
$('#objective-prompt-extension-prompt').val(objectivePrompts.currentTask)
}
function populateCustomPrompts(){
// Populate saved prompts
$('#objective-custom-prompt-select').empty()
for (const customPromptName in extension_settings.objective.customPrompts){
const option = document.createElement('option');
option.innerText = customPromptName;
option.value = customPromptName;
option.selected = customPromptName
$('#objective-custom-prompt-select').append(option)
}
}
//###############################//
//# UI AND Settings #//
//###############################//
const defaultSettings = {
currentObjectiveId: null,
taskTree: null,
chatDepth: 2,
checkFrequency: 3,
hideTasks: false,
prompts: defaultPrompts,
}
// Convenient single call. Not much at the moment.
function resetState() {
lastMessageWasSwipe = false
loadSettings();
}
//
function saveState() {
const context = getContext();
if (currentChatId == "") {
currentChatId = context.chatId
}
chat_metadata['objective'] = {
currentObjectiveId: currentObjective.id,
taskTree: taskTree.toSaveStateRecurse(),
checkFrequency: $('#objective-check-frequency').val(),
chatDepth: $('#objective-chat-depth').val(),
hideTasks: $('#objective-hide-tasks').prop('checked'),
prompts: objectivePrompts,
}
saveMetadataDebounced();
}
// Dump core state
function debugObjectiveExtension() {
console.log(JSON.stringify({
"currentTask": currentTask,
"currentObjective": currentObjective,
"taskTree": taskTree.toSaveStateRecurse(),
"chat_metadata": chat_metadata['objective'],
"extension_settings": extension_settings['objective'],
"prompts": objectivePrompts
}, null, 2))
}
window.debugObjectiveExtension = debugObjectiveExtension
// Populate UI task list
function updateUiTaskList() {
$('#objective-tasks').empty()
// Show button to navigate back to parent objective if parent exists
if (currentObjective){
if (currentObjective.parentId !== "") {
$('#objective-parent').show()
} else {
$('#objective-parent').hide()
}
}
$('#objective-text').val(currentObjective.description)
if (currentObjective.children.length > 0){
// Show tasks if there are any to show
for (const task of currentObjective.children) {
task.addUiElement()
}
} else {
// Show button to add tasks if there are none
$('#objective-tasks').append(`
<input id="objective-task-add-first" type="button" class="menu_button" value="Add Task">
`)
$("#objective-task-add-first").on('click', () => {
currentObjective.addTask("")
setCurrentTask()
updateUiTaskList()
})
}
}
function onParentClick() {
currentObjective = getTaskById(currentObjective.parentId)
updateUiTaskList()
setCurrentTask()
}
// Trigger creation of new tasks with given objective.
async function onGenerateObjectiveClick() {
await generateTasks()
saveState()
}
// Update extension prompts
function onChatDepthInput() {
saveState()
setCurrentTask() // Ensure extension prompt is updated
}
function onObjectiveTextFocusOut(){
if (currentObjective){
currentObjective.description = $('#objective-text').val()
saveState()
}
}
// Update how often we check for task completion
function onCheckFrequencyInput() {
checkCounter = $("#objective-check-frequency").val()
$('#objective-counter').text(checkCounter)
saveState()
}
function onHideTasksInput() {
$('#objective-tasks').prop('hidden', $('#objective-hide-tasks').prop('checked'))
saveState()
}
function loadTaskChildrenRecurse(savedTask) {
let tempTaskTree = new ObjectiveTask({
id: savedTask.id,
description: savedTask.description,
completed: savedTask.completed,
parentId: savedTask.parentId,
})
for (const task of savedTask.children){
const childTask = loadTaskChildrenRecurse(task)
tempTaskTree.children.push(childTask)
}
return tempTaskTree
}
function loadSettings() {
// Load/Init settings for chatId
currentChatId = getContext().chatId
// Reset Objectives and Tasks in memory
taskTree = null;
currentObjective = null;
// Init extension settings
if (Object.keys(extension_settings.objective).length === 0) {
Object.assign(extension_settings.objective, { 'customPrompts': {'default':defaultPrompts}})
}
// Bail on home screen
if (currentChatId == undefined) {
return
}
// Migrate existing settings
if (currentChatId in extension_settings.objective) {
// TODO: Remove this soon
chat_metadata['objective'] = extension_settings.objective[currentChatId];
delete extension_settings.objective[currentChatId];
}
if (!('objective' in chat_metadata)) {
Object.assign(chat_metadata, { objective: defaultSettings });
}
// Migrate legacy flat objective to new objectiveTree and currentObjective
if ('objective' in chat_metadata.objective) {
// Create root objective from legacy objective
taskTree = new ObjectiveTask({id:0, description: chat_metadata.objective.objective});
currentObjective = taskTree;
// Populate root objective tree from legacy tasks
if ('tasks' in chat_metadata.objective) {
let idIncrement = 0;
taskTree.children = chat_metadata.objective.tasks.map(task => {
idIncrement += 1;
return new ObjectiveTask({
id: idIncrement,
description: task.description,
completed: task.completed,
parentId: taskTree.id,
})
});
}
saveState();
delete chat_metadata.objective.objective;
delete chat_metadata.objective.tasks;
} else {
// Load Objectives and Tasks (Normal path)
if (chat_metadata.objective.taskTree){
taskTree = loadTaskChildrenRecurse(chat_metadata.objective.taskTree)
}
}
// Make sure there's a root task
if (!taskTree) {
taskTree = new ObjectiveTask({id:0,description:$('#objective-text').val()})
}
currentObjective = taskTree
checkCounter = chat_metadata['objective'].checkFrequency
// Update UI elements
$('#objective-counter').text(checkCounter)
$("#objective-text").text(taskTree.description)
updateUiTaskList()
$('#objective-chat-depth').val(chat_metadata['objective'].chatDepth)
$('#objective-check-frequency').val(chat_metadata['objective'].checkFrequency)
$('#objective-hide-tasks').prop('checked', chat_metadata['objective'].hideTasks)
$('#objective-tasks').prop('hidden', $('#objective-hide-tasks').prop('checked'))
setCurrentTask(null, true)
}
function addManualTaskCheckUi() {
$('#extensionsMenu').prepend(`
<div id="objective-task-manual-check-menu-item" class="list-group-item flex-container flexGap5">
<div id="objective-task-manual-check" class="extensionsMenuExtensionButton fa-regular fa-square-check"/></div>
Manual Task Check
</div>`)
$('#objective-task-manual-check-menu-item').attr('title', 'Trigger AI check of completed tasks').on('click', checkTaskCompleted)
}
jQuery(() => {
const settingsHtml = `
<div class="objective-settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>Objective</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<label for="objective-text"><small>Enter an objective and generate tasks. The AI will attempt to complete tasks autonomously</small></label>
<textarea id="objective-text" type="text" class="text_pole textarea_compact" rows="4"></textarea>
<div class="objective_block flex-container">
<input id="objective-generate" class="menu_button" type="submit" value="Auto-Generate Tasks" />
<label class="checkbox_label"><input id="objective-hide-tasks" type="checkbox"> Hide Tasks</label>
</div>
<div id="objective-parent" class="objective_block flex-container">
<i class="objective-task-button fa-solid fa-circle-left fa-2x" title="Go to Parent"></i>
<small>Go to parent task</small>
</div>
<div id="objective-tasks"> </div>
<div class="objective_block margin-bot-10px">
<div class="objective_block objective_block_control flex1 flexFlowColumn">
<label for="objective-chat-depth">Position in Chat</label>
<input id="objective-chat-depth" class="text_pole widthUnset" type="number" min="0" max="99" />
</div>
<br>
<div class="objective_block objective_block_control flex1">
<label for="objective-check-frequency">Task Check Frequency</label>
<input id="objective-check-frequency" class="text_pole widthUnset" type="number" min="0" max="99" />
<small>(0 = disabled)</small>
</div>
</div>
<span> Messages until next AI task completion check <span id="objective-counter">0</span></span>
<div class="objective_block flex-container">
<input id="objective_prompt_edit" class="menu_button" type="submit" value="Edit Prompts" />
</div>
<hr class="sysHR">
</div>
</div>
</div>
`;
addManualTaskCheckUi()
$('#extensions_settings').append(settingsHtml);
$('#objective-generate').on('click', onGenerateObjectiveClick)
$('#objective-chat-depth').on('input', onChatDepthInput)
$("#objective-check-frequency").on('input', onCheckFrequencyInput)
$('#objective-hide-tasks').on('click', onHideTasksInput)
$('#objective_prompt_edit').on('click', onEditPromptClick)
$('#objective-parent').hide()
$('#objective-parent').on('click',onParentClick)
$('#objective-text').on('focusout',onObjectiveTextFocusOut)
loadSettings()
eventSource.on(event_types.CHAT_CHANGED, () => {
resetState()
});
eventSource.on(event_types.MESSAGE_SWIPED, () => {
lastMessageWasSwipe = true
})
eventSource.on(event_types.MESSAGE_RECEIVED, () => {
if (currentChatId == undefined || jQuery.isEmptyObject(currentTask) || lastMessageWasSwipe) {
lastMessageWasSwipe = false
return
}
if ($("#objective-check-frequency").val() > 0) {
// Check only at specified interval
if (checkCounter <= 0) {
checkTaskCompleted();
}
checkCounter -= 1
}
setCurrentTask();
$('#objective-counter').text(checkCounter)
});
registerSlashCommand('taskcheck', checkTaskCompleted, [], ' checks if the current task is completed', true, true);
});

View File

@@ -1,11 +0,0 @@
{
"display_name": "Objective",
"loading_order": 5,
"requires": [],
"optional": [],
"js": "index.js",
"css": "style.css",
"author": "Ouoertheo",
"version": "0.0.1",
"homePage": ""
}

View File

@@ -1,52 +0,0 @@
#objective-counter {
font-weight: 600;
color: orange;
}
.objective_block {
display: flex;
align-items: center;
column-gap: 5px;
flex-wrap: wrap;
}
.objective_prompt_block {
display: flex;
align-items: baseline;
column-gap: 5px;
flex-wrap: wrap;
}
.objective_block_control {
align-items: baseline;
}
.objective_block_control small,
.objective_block_control label {
width: max-content;
}
.objective-task-button {
margin: 0;
outline: none;
border: none;
cursor: pointer;
transition: 0.3s;
opacity: 0.7;
align-items: center;
justify-content: center;
}
.objective-task-button:hover {
opacity: 1;
}
[id^=objective-task-delete-] {
color: #da3f3f;
}
#objective-tasks span {
margin: unset;
margin-bottom: 5px !important;
}

View File

@@ -1,8 +1,7 @@
import { saveSettingsDebounced, callPopup, getRequestHeaders, substituteParams } from "../../../script.js";
import { getContext, extension_settings } from "../../extensions.js";
import { initScrollHeight, resetScrollHeight } from "../../utils.js";
import { executeSlashCommands, getSlashCommandsHelp, registerSlashCommand } from "../../slash-commands.js";
import { executeSlashCommands, registerSlashCommand } from "../../slash-commands.js";
export { MODULE_NAME };
@@ -15,13 +14,14 @@ const defaultSettings = {
quickReplyEnabled: false,
numberOfSlots: 5,
quickReplySlots: [],
placeBeforePromptEnabled: false,
placeBeforeInputEnabled: false,
quickActionEnabled: false,
AutoInputInject: true,
}
//method from worldinfo
async function updateQuickReplyPresetList() {
var result = await fetch("/getsettings", {
const result = await fetch("/getsettings", {
method: "POST",
headers: getRequestHeaders(),
body: JSON.stringify({}),
@@ -35,8 +35,12 @@ async function updateQuickReplyPresetList() {
if (presets !== undefined) {
presets.forEach((item, i) => {
$("#quickReplyPresets").append(`<option value='${item.name}'${selected_preset.includes(item.name) ? ' selected' : ''}>${item.name}</option>`);
presets.forEach((item) => {
const option = document.createElement('option');
option.value = item.name;
option.innerText = item.name;
option.selected = selected_preset.includes(item.name);
$("#quickReplyPresets").append(option);
});
}
}
@@ -50,6 +54,10 @@ async function loadSettings(type) {
Object.assign(extension_settings.quickReply, defaultSettings);
}
if (extension_settings.quickReply.AutoInputInject === undefined) {
extension_settings.quickReply.AutoInputInject = true;
}
// If the user has an old version of the extension, update it
if (!Array.isArray(extension_settings.quickReply.quickReplySlots)) {
extension_settings.quickReply.quickReplySlots = [];
@@ -77,20 +85,21 @@ async function loadSettings(type) {
$('#quickReplyEnabled').prop('checked', extension_settings.quickReply.quickReplyEnabled);
$('#quickReplyNumberOfSlots').val(extension_settings.quickReply.numberOfSlots);
$('#placeBeforePromptEnabled').prop('checked', extension_settings.quickReply.placeBeforePromptEnabled);
$('#placeBeforeInputEnabled').prop('checked', extension_settings.quickReply.placeBeforeInputEnabled);
$('#quickActionEnabled').prop('checked', extension_settings.quickReply.quickActionEnabled);
$('#AutoInputInject').prop('checked', extension_settings.quickReply.AutoInputInject);
}
function onQuickReplyInput(id) {
extension_settings.quickReply.quickReplySlots[id - 1].mes = $(`#quickReply${id}Mes`).val();
$(`#quickReply${id}`).attr('title', ($(`#quickReply${id}Mes`).val()));
$(`#quickReply${id}`).attr('title', String($(`#quickReply${id}Mes`).val()));
resetScrollHeight($(`#quickReply${id}Mes`));
saveSettingsDebounced();
}
function onQuickReplyLabelInput(id) {
extension_settings.quickReply.quickReplySlots[id - 1].label = $(`#quickReply${id}Label`).val();
$(`#quickReply${id}`).text($(`#quickReply${id}Label`).val());
$(`#quickReply${id}`).text(String($(`#quickReply${id}Label`).val()));
saveSettingsDebounced();
}
@@ -109,8 +118,13 @@ async function onQuickActionEnabledInput() {
saveSettingsDebounced();
}
async function onPlaceBeforePromptEnabledInput() {
extension_settings.quickReply.placeBeforePromptEnabled = !!$(this).prop('checked');
async function onPlaceBeforeInputEnabledInput() {
extension_settings.quickReply.placeBeforeInputEnabled = !!$(this).prop('checked');
saveSettingsDebounced();
}
async function onAutoInputInject() {
extension_settings.quickReply.AutoInputInject = !!$(this).prop('checked');
saveSettingsDebounced();
}
@@ -125,28 +139,32 @@ async function sendQuickReply(index) {
let newText;
if (existingText) {
// If existing text, add space after prompt
if (extension_settings.quickReply.placeBeforePromptEnabled) {
if (existingText && extension_settings.quickReply.AutoInputInject) {
if (extension_settings.quickReply.placeBeforeInputEnabled) {
newText = `${prompt} ${existingText} `;
} else {
newText = `${existingText} ${prompt} `;
}
} else {
// If no existing text, add prompt only (with a trailing space)
newText = prompt + ' ';
// If no existing text and placeBeforeInputEnabled false, add prompt only (with a trailing space)
newText = `${prompt} `;
}
newText = substituteParams(newText);
// the prompt starts with '/' - execute slash commands natively
if (prompt.startsWith('/')) {
await executeSlashCommands(newText);
return;
}
$("#send_textarea").val(newText);
// Set the focus back to the textarea
$("#send_textarea").focus();
$("#send_textarea").trigger('focus');
// Only trigger send button if quickActionEnabled is not checked or
// the prompt starts with '/'
if (!extension_settings.quickReply.quickActionEnabled || prompt.startsWith('/')) {
// Only trigger send button if quickActionEnabled is not checked or
if (!extension_settings.quickReply.quickActionEnabled) {
$("#send_but").trigger('click');
}
}
@@ -199,7 +217,8 @@ async function saveQuickReplyPreset() {
quickReplyEnabled: extension_settings.quickReply.quickReplyEnabled,
quickReplySlots: extension_settings.quickReply.quickReplySlots,
numberOfSlots: extension_settings.quickReply.numberOfSlots,
selectedPreset: name
AutoInputInject: extension_settings.quickReply.AutoInputInject,
selectedPreset: name,
}
const response = await fetch('/savequickreply', {
@@ -221,7 +240,7 @@ async function saveQuickReplyPreset() {
}
else {
presets[quickReplyPresetIndex] = quickReplyPreset;
$(`#quickReplyPresets option[value="${name}"]`).attr('selected', true);
$(`#quickReplyPresets option[value="${name}"]`).prop('selected', true);
}
saveSettingsDebounced();
} else {
@@ -274,8 +293,8 @@ function generateQuickReplyElements() {
for (let i = 1; i <= extension_settings.quickReply.numberOfSlots; i++) {
quickReplyHtml += `
<div class="flex-container alignitemsflexstart">
<input class="text_pole wide30p" id="quickReply${i}Label" placeholder="(Add a button label)">
<textarea id="quickReply${i}Mes" placeholder="(custom message here)" class="text_pole widthUnset flex1" rows="2"></textarea>
<input class="text_pole wide30p" id="quickReply${i}Label" placeholder="(Button label)">
<textarea id="quickReply${i}Mes" placeholder="(Custom message or /command)" class="text_pole widthUnset flex1" rows="2"></textarea>
</div>
`;
}
@@ -309,7 +328,7 @@ async function applyQuickReplyPreset(name) {
addQuickReplyBar();
moduleWorker();
$(`#quickReplyPresets option[value="${name}"]`).attr('selected', true);
$(`#quickReplyPresets option[value="${name}"]`).prop('selected', true);
console.debug('QR Preset applied: ' + name);
}
@@ -334,7 +353,6 @@ async function doQR(_, text) {
}
jQuery(async () => {
moduleWorker();
setInterval(moduleWorker, UPDATE_INTERVAL);
const settingsHtml = `
@@ -348,20 +366,28 @@ jQuery(async () => {
<div>
<label class="checkbox_label">
<input id="quickReplyEnabled" type="checkbox" />
Enable Quick Replies
Enable Quick Replies
</label>
<label class="checkbox_label">
<input id="quickActionEnabled" type="checkbox" />
Disable Send / Insert In User Input
Disable Send / Insert In User Input
</label>
<label class="checkbox_label marginBot10">
<input id="placeBeforePromptEnabled" type="checkbox" />
Place Quick-reply before the Prompt
<input id="placeBeforeInputEnabled" type="checkbox" />
Place Quick-reply before the Input
</label>
<label class="checkbox_label marginBot10">
<input id="AutoInputInject" type="checkbox" />
Inject user input automatically<br>(If disabled, use {{input}} macro for manual injection)
</label>
<label for="quickReplyPresets">Quick Reply presets:</label>
<div class="flex-container flexnowrap wide100p">
<select id="quickReplyPresets" name="quickreply-preset">
<select id="quickReplyPresets" name="quickreply-preset" class="flex1 text_pole">
</select>
<i id="quickReplyPresetSaveButton" class="fa-solid fa-save"></i>
<div id="quickReplyPresetSaveButton" class="menu_button menu_button_icon">
<div class="fa-solid fa-save"></div>
<span>Save</span>
</div>
</div>
<label for="quickReplyNumberOfSlots">Number of slots:</label>
</div>
@@ -379,10 +405,11 @@ jQuery(async () => {
</div>`;
$('#extensions_settings2').append(settingsHtml);
// Add event handler for quickActionEnabled
$('#quickActionEnabled').on('input', onQuickActionEnabledInput);
$('#placeBeforePromptEnabled').on('input', onPlaceBeforePromptEnabledInput);
$('#placeBeforeInputEnabled').on('input', onPlaceBeforeInputEnabledInput);
$('#AutoInputInject').on('input', onAutoInputInject);
$('#quickReplyEnabled').on('input', onQuickReplyEnabledInput);
$('#quickReplyNumberOfSlotsApply').on('click', onQuickReplyNumberOfSlotsInput);
$("#quickReplyPresetSaveButton").on('click', saveQuickReplyPreset);
@@ -392,16 +419,13 @@ jQuery(async () => {
extension_settings.quickReplyPreset = quickReplyPresetSelected;
applyQuickReplyPreset(quickReplyPresetSelected);
saveSettingsDebounced();
});
await loadSettings('init');
addQuickReplyBar();
});
$(document).ready(() => {
jQuery(() => {
registerSlashCommand('qr', doQR, [], '<span class="monospace">(number)</span> activates the specified Quick Reply', true, true);
registerSlashCommand('qrset', doQRPresetSwitch, [], '<span class="monospace">(name)</span> swaps to the specified Quick Reply Preset', true, true);
})

View File

@@ -1,152 +0,0 @@
import { saveSettingsDebounced } from "../../../script.js";
import { extension_settings } from "../../extensions.js";
function toggleRandomizedSetting(buttonRef, forId) {
if (extension_settings.randomizer.controls.indexOf(forId) === -1) {
extension_settings.randomizer.controls.push(forId);
} else {
extension_settings.randomizer.controls = extension_settings.randomizer.controls.filter(x => x !== forId);
}
buttonRef.toggleClass('active');
console.debug('Randomizer controls:', extension_settings.randomizer.controls);
saveSettingsDebounced();
}
function addRandomizeButton() {
const counterRef = $(this);
const labelRef = $(this).find('div[data-for]');
const isDisabled = counterRef.data('randomization-disabled');
if (labelRef.length === 0 || isDisabled == true) {
return;
}
const forId = labelRef.data('for');
const buttonRef = $('<div class="randomize_button menu_button fa-solid fa-shuffle"></div>');
buttonRef.toggleClass('active', extension_settings.randomizer.controls.indexOf(forId) !== -1);
buttonRef.hide();
buttonRef.on('click', () => toggleRandomizedSetting(buttonRef, forId));
counterRef.append(buttonRef);
}
function onRandomizerEnabled() {
extension_settings.randomizer.enabled = $(this).prop('checked');
$('.randomize_button').toggle(extension_settings.randomizer.enabled);
console.debug('Randomizer enabled:', extension_settings.randomizer.enabled);
}
window['randomizerInterceptor'] = (function () {
if (extension_settings.randomizer.enabled === false) {
console.debug('Randomizer skipped: disabled.');
return;
}
if (extension_settings.randomizer.fluctuation === 0 || extension_settings.randomizer.controls.length === 0) {
console.debug('Randomizer skipped: nothing to do.');
return;
}
for (const control of extension_settings.randomizer.controls) {
const controlRef = $('#' + control);
if (controlRef.length === 0) {
console.debug(`Randomizer skipped: control ${control} not found.`);
continue;
}
if (!controlRef.is(':visible')) {
console.debug(`Randomizer skipped: control ${control} is not visible.`);
continue;
}
let previousValue = parseFloat(controlRef.data('previous-value'));
let originalValue = parseFloat(controlRef.data('original-value'));
let currentValue = parseFloat(controlRef.val());
let value;
// Initialize originalValue and previousValue if they are NaN
if (isNaN(originalValue)) {
originalValue = currentValue;
controlRef.data('original-value', originalValue);
}
if (isNaN(previousValue)) {
previousValue = currentValue;
controlRef.data('previous-value', previousValue);
}
// If the current value hasn't changed compared to the previous value, use the original value as a base for the calculation
if (currentValue === previousValue) {
console.debug(`Randomizer for ${control} reusing original value: ${originalValue}`);
value = originalValue;
} else {
console.debug(`Randomizer for ${control} using current value: ${currentValue}`);
value = currentValue;
controlRef.data('previous-value', currentValue); // Update the previous value when using the current value
controlRef.data('original-value', currentValue); // Update the original value when using the current value
}
if (isNaN(value)) {
console.debug('Randomizer skipped: NaN.');
continue;
}
const fluctuation = extension_settings.randomizer.fluctuation;
const min = parseFloat(controlRef.attr('min'));
const max = parseFloat(controlRef.attr('max'));
const delta = (Math.random() * fluctuation * 2 - fluctuation) * value;
const newValue = Math.min(Math.max(value + delta, min), max);
console.debug(`Randomizer for ${control}: ${value} -> ${newValue} (delta: ${delta}, min: ${min}, max: ${max})`);
controlRef.val(newValue).trigger('input');
controlRef.data('previous-value', parseFloat(controlRef.val()));
}
});
jQuery(() => {
const html = `
<div class="randomizer_settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>Parameter Randomizer</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<label for="randomizer_enabled" class="checkbox_label">
<input type="checkbox" id="randomizer_enabled" name="randomizer_enabled" >
Enabled
</label>
<div class="range-block">
<div class="range-block-title">
Fluctuation (0-1)
</div>
<div class="range-block-range-and-counter">
<div class="range-block-range-and-counter">
<div class="range-block-range">
<input type="range" id="randomizer_fluctuation" min="0" max="1" step="0.1">
</div>
<div class="range-block-counter">
<div contenteditable="true" data-for="randomizer_fluctuation" id="randomizer_fluctuation_counter">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>`;
$('#extensions_settings2').append(html);
$('#ai_response_configuration .range-block-counter').each(addRandomizeButton);
$('#randomizer_enabled').on('input', onRandomizerEnabled);
$('#randomizer_enabled').prop('checked', extension_settings.randomizer.enabled).trigger('input');
$('#randomizer_fluctuation').val(extension_settings.randomizer.fluctuation).trigger('input');
$('#randomizer_fluctuation_counter').text(extension_settings.randomizer.fluctuation);
$('#randomizer_fluctuation').on('input', function () {
const value = parseFloat($(this).val());
$('#randomizer_fluctuation_counter').text(value);
extension_settings.randomizer.fluctuation = value;
console.debug('Randomizer fluctuation:', extension_settings.randomizer.fluctuation);
saveSettingsDebounced();
});
});

View File

@@ -1,12 +0,0 @@
{
"display_name": "Parameter Randomizer",
"loading_order": 15,
"requires": [],
"optional": [],
"js": "index.js",
"css": "style.css",
"author": "Cohee#1207",
"version": "1.0.0",
"generate_interceptor": "randomizerInterceptor",
"homePage": "https://github.com/SillyTavern/SillyTavern"
}

View File

@@ -33,7 +33,7 @@
<small data-i18n="Replace With">Replace With</small>
</label>
<div>
<textarea
<textarea
class="regex_replace_string text_pole wide100p textarea_compact"
placeholder="Use {{match}} to include the matched text from the Find Regex"
rows="2"
@@ -45,7 +45,7 @@
<small data-i18n="Trim Out">Trim Out</small>
</label>
<div>
<textarea
<textarea
class="regex_trim_strings text_pole wide100p textarea_compact"
placeholder="Globally trims any unwanted parts from a regex match before replacement. Separate each element by an enter."
rows="3"
@@ -86,6 +86,10 @@
<input type="checkbox" name="only_format_display" />
<span data-i18n="Only Format Display">Only Format Display</span>
</label>
<label class="checkbox flex-container" title="Chat history won't change, only the prompt as the request is sent (on generation)">
<input type="checkbox" name="only_format_prompt"/>
<span data-i18n="Only Format Prompt (?)">Only Format Prompt (?)</span>
</label>
<label class="checkbox flex-container">
<input type="checkbox" name="run_on_edit" />
<span data-i18n="Run On Edit">Run On Edit</span>

View File

@@ -24,12 +24,12 @@ function regexFromString(input) {
try {
// Parse input
var m = input.match(/(\/?)(.+)\1([a-z]*)/i);
// Invalid flags
if (m[3] && !/^(?!.*?(.).*?\1)[gmixXsuUAJ]+$/.test(m[3])) {
return RegExp(input);
}
// Create the regular expression
return new RegExp(m[2], m[3]);
} catch {
@@ -38,19 +38,24 @@ function regexFromString(input) {
}
// Parent function to fetch a regexed version of a raw string
function getRegexedString(rawString, placement, { characterOverride, isMarkdown } = {}) {
function getRegexedString(rawString, placement, { characterOverride, isMarkdown, isPrompt } = {}) {
let finalString = rawString;
if (extension_settings.disabledExtensions.includes("regex") || !rawString || placement === undefined) {
return finalString;
}
extension_settings.regex.forEach((script) => {
if ((script.markdownOnly && !isMarkdown) || (!script.markdownOnly && isMarkdown)) {
return;
}
if (script.placement.includes(placement)) {
finalString = runRegexScript(script, finalString, { characterOverride });
if (
// Script applies to Markdown and input is Markdown
(script.markdownOnly && isMarkdown) ||
// Script applies to Generate and input is Generate
(script.promptOnly && isPrompt) ||
// Script applies to all cases when neither "only"s are true, but there's no need to do it when `isMarkdown`, the as source (chat history) should already be changed beforehand
(!script.markdownOnly && !script.promptOnly && !isMarkdown)
) {
if (script.placement.includes(placement)) {
finalString = runRegexScript(script, finalString, { characterOverride });
}
}
});
@@ -91,7 +96,7 @@ function runRegexScript(regexScript, rawString, { characterOverride } = {}) {
const subReplaceString = substituteRegexParams(
regexScript.replaceString,
trimCapturedMatch ?? trimFencedMatch,
{
{
characterOverride,
replaceStrategy: regexScript.replaceStrategy ?? regex_replace_strategy.REPLACE
}

View File

@@ -76,10 +76,27 @@ async function loadRegexScripts() {
const scriptHtml = scriptTemplate.clone();
scriptHtml.attr('id', uuidv4());
scriptHtml.find('.regex_script_name').text(script.scriptName);
scriptHtml.find('.edit_existing_regex').on('click', async function() {
scriptHtml.find('.disable_regex').prop("checked", script.disabled ?? false)
.on('input', function () {
script.disabled = !!$(this).prop("checked");
saveSettingsDebounced();
});
scriptHtml.find('.regex-toggle-on').on('click', function () {
scriptHtml.find('.disable_regex').prop("checked", true).trigger('input');
});
scriptHtml.find('.regex-toggle-off').on('click', function () {
scriptHtml.find('.disable_regex').prop("checked", false).trigger('input');
});
scriptHtml.find('.edit_existing_regex').on('click', async function () {
await onRegexEditorOpenClick(scriptHtml.attr("id"));
});
scriptHtml.find('.delete_regex').on('click', async function() {
scriptHtml.find('.delete_regex').on('click', async function () {
const confirm = await callPopup("Are you sure you want to delete this regex script?", "confirm");
if (!confirm) {
return;
}
await deleteRegexScript({ existingId: scriptHtml.attr("id") });
});
@@ -113,6 +130,9 @@ async function onRegexEditorOpenClick(existingId) {
editorHtml
.find(`input[name="only_format_display"]`)
.prop("checked", existingScript.markdownOnly ?? false);
editorHtml
.find(`input[name="only_format_prompt"]`)
.prop("checked", existingScript.promptOnly ?? false);
editorHtml
.find(`input[name="run_on_edit"]`)
.prop("checked", existingScript.runOnEdit ?? false);
@@ -154,7 +174,7 @@ async function onRegexEditorOpenClick(existingId) {
editorHtml
.find(`input[name="replace_position"]`)
.filter(":checked")
.map(function() { return parseInt($(this).val()) })
.map(function () { return parseInt($(this).val()) })
.get()
.filter((e) => e !== NaN) || [],
disabled:
@@ -165,6 +185,10 @@ async function onRegexEditorOpenClick(existingId) {
editorHtml
.find(`input[name="only_format_display"]`)
.prop("checked"),
promptOnly:
editorHtml
.find(`input[name="only_format_prompt"]`)
.prop("checked"),
runOnEdit:
editorHtml
.find(`input[name="run_on_edit"]`)
@@ -197,6 +221,7 @@ function migrateSettings() {
script.placement = script.placement.filter((e) => e !== regex_placement.MD_DISPLAY);
script.markdownOnly = true
script.promptOnly = true
performSave = true;
}
@@ -231,7 +256,7 @@ jQuery(async () => {
const settingsHtml = await $.get("scripts/extensions/regex/dropdown.html");
$("#extensions_settings2").append(settingsHtml);
$("#open_regex_editor").on("click", function() {
$("#open_regex_editor").on("click", function () {
onRegexEditorOpenClick(false);
});

View File

@@ -2,6 +2,11 @@
<span class="drag-handle menu-handle">&#9776;</span>
<div class="regex_script_name flexGrow overflow-hidden"></div>
<div class="flex-container flexnowrap">
<label class="checkbox flex-container" for="regex_disable">
<input type="checkbox" name="regex_disable" class="disable_regex" />
<span class="regex-toggle-on fa-solid fa-toggle-on" title="Disable script"></span>
<span class="regex-toggle-off fa-solid fa-toggle-off" title="Enable script"></span>
</label>
<div class="edit_existing_regex menu_button">
<i class="fa-solid fa-pencil"></i>
</div>

View File

@@ -5,6 +5,10 @@
flex-direction: row;
}
.regex_settings .checkbox {
align-items: center;
}
.regex-script-container {
margin-top: 10px;
margin-bottom: 10px;
@@ -17,4 +21,34 @@
padding: 0 5px;
margin-top: 1px;
margin-bottom: 1px;
}
}
input.disable_regex {
display: none !important;
}
.regex-toggle-off {
cursor: pointer;
opacity: 0.5;
filter: grayscale(0.5);
}
.regex-toggle-on {
cursor: pointer;
}
.disable_regex:checked ~ .regex-toggle-off {
display: block;
}
.disable_regex:checked ~ .regex-toggle-on {
display: none;
}
.disable_regex:not(:checked) ~ .regex-toggle-off {
display: none;
}
.disable_regex:not(:checked) ~ .regex-toggle-on {
display: block;
}

View File

@@ -1,489 +0,0 @@
/*
TODO:
- load RVC models list from extras
- Settings per characters
*/
import { saveSettingsDebounced } from "../../../script.js";
import { getContext, getApiUrl, extension_settings, doExtrasFetch, ModuleWorkerWrapper, modules } from "../../extensions.js";
export { MODULE_NAME, rvcVoiceConversion };
const MODULE_NAME = 'RVC';
const DEBUG_PREFIX = "<RVC module> "
const UPDATE_INTERVAL = 1000
let charactersList = [] // Updated with module worker
let rvcModelsList = [] // Initialized only once
let rvcModelsReceived = false;
function updateVoiceMapText() {
let voiceMapText = ""
for (let i in extension_settings.rvc.voiceMap) {
const voice_settings = extension_settings.rvc.voiceMap[i];
voiceMapText += i + ":"
+ voice_settings["modelName"] + "("
+ voice_settings["pitchExtraction"] + ","
+ voice_settings["pitchOffset"] + ","
+ voice_settings["indexRate"] + ","
+ voice_settings["filterRadius"] + ","
+ voice_settings["rmsMixRate"] + ","
+ voice_settings["protect"]
+ "),\n"
}
extension_settings.rvc.voiceMapText = voiceMapText;
$('#rvc_voice_map').val(voiceMapText);
console.debug(DEBUG_PREFIX, "Updated voice map debug text to\n", voiceMapText)
}
//#############################//
// Extension UI and Settings //
//#############################//
const defaultSettings = {
enabled: false,
model: "",
pitchOffset: 0,
pitchExtraction: "dio",
indexRate: 0.88,
filterRadius: 3,
rmsMixRate: 1,
protect: 0.33,
voicMapText: "",
voiceMap: {}
}
function loadSettings() {
if (extension_settings.rvc === undefined)
extension_settings.rvc = {};
if (Object.keys(extension_settings.rvc).length === 0) {
Object.assign(extension_settings.rvc, defaultSettings)
}
$('#rvc_enabled').prop('checked', extension_settings.rvc.enabled);
$('#rvc_model').val(extension_settings.rvc.model);
$('#rvc_pitch_extraction').val(extension_settings.rvc.pitchExtraction);
$('#rvc_pitch_extractiont_value').text(extension_settings.rvc.pitchExtraction);
$('#rvc_index_rate').val(extension_settings.rvc.indexRate);
$('#rvc_index_rate_value').text(extension_settings.rvc.indexRate);
$('#rvc_filter_radius').val(extension_settings.rvc.filterRadius);
$("#rvc_filter_radius_value").text(extension_settings.rvc.filterRadius);
$('#rvc_pitch_offset').val(extension_settings.rvc.pitchOffset);
$('#rvc_pitch_offset_value').text(extension_settings.rvc.pitchOffset);
$('#rvc_rms_mix_rate').val(extension_settings.rvc.rmsMixRate);
$("#rvc_rms_mix_rate_value").text(extension_settings.rvc.rmsMixRate);
$('#rvc_protect').val(extension_settings.rvc.protect);
$("#rvc_protect_value").text(extension_settings.rvc.protect);
$('#rvc_voice_map').val(extension_settings.rvc.voiceMapText);
}
async function onEnabledClick() {
extension_settings.rvc.enabled = $('#rvc_enabled').is(':checked');
saveSettingsDebounced()
}
async function onPitchExtractionChange() {
extension_settings.rvc.pitchExtraction = $('#rvc_pitch_extraction').val();
saveSettingsDebounced()
}
async function onIndexRateChange() {
extension_settings.rvc.indexRate = Number($('#rvc_index_rate').val());
$("#rvc_index_rate_value").text(extension_settings.rvc.indexRate)
saveSettingsDebounced()
}
async function onFilterRadiusChange() {
extension_settings.rvc.filterRadius = Number($('#rvc_filter_radius').val());
$("#rvc_filter_radius_value").text(extension_settings.rvc.filterRadius)
saveSettingsDebounced()
}
async function onPitchOffsetChange() {
extension_settings.rvc.pitchOffset = Number($('#rvc_pitch_offset').val());
$("#rvc_pitch_offset_value").text(extension_settings.rvc.pitchOffset)
saveSettingsDebounced()
}
async function onRmsMixRateChange() {
extension_settings.rvc.rmsMixRate = Number($('#rvc_rms_mix_rate').val());
$("#rvc_rms_mix_rate_value").text(extension_settings.rvc.rmsMixRate)
saveSettingsDebounced()
}
async function onProtectChange() {
extension_settings.rvc.protect = Number($('#rvc_protect').val());
$("#rvc_protect_value").text(extension_settings.rvc.protect)
saveSettingsDebounced()
}
async function onApplyClick() {
let error = false;
const character = $("#rvc_character_select").val();
const model_name = $("#rvc_model_select").val();
const pitchExtraction = $("#rvc_pitch_extraction").val();
const indexRate = $("#rvc_index_rate").val();
const filterRadius = $("#rvc_filter_radius").val();
const pitchOffset = $("#rvc_pitch_offset").val();
const rmsMixRate = $("#rvc_rms_mix_rate").val();
const protect = $("#rvc_protect").val();
if (character === "none") {
toastr.error("Character not selected.", DEBUG_PREFIX + " voice mapping apply", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
return;
}
if (model_name == "none") {
toastr.error("Model not selected.", DEBUG_PREFIX + " voice mapping apply", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
return;
}
extension_settings.rvc.voiceMap[character] = {
"modelName": model_name,
"pitchExtraction": pitchExtraction,
"indexRate": indexRate,
"filterRadius": filterRadius,
"pitchOffset": pitchOffset,
"rmsMixRate": rmsMixRate,
"protect": protect
}
updateVoiceMapText();
console.debug(DEBUG_PREFIX, "Updated settings of ", character, ":", extension_settings.rvc.voiceMap[character])
saveSettingsDebounced();
}
async function onDeleteClick() {
const character = $("#rvc_character_select").val();
if (character === "none") {
toastr.error("Character not selected.", DEBUG_PREFIX + " voice mapping delete", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
return;
}
delete extension_settings.rvc.voiceMap[character];
console.debug(DEBUG_PREFIX, "Deleted settings of ", character);
updateVoiceMapText();
saveSettingsDebounced();
}
async function onChangeUploadFiles() {
const url = new URL(getApiUrl());
const inputFiles = $("#rvc_model_upload_files").get(0).files;
let formData = new FormData();
for (const file of inputFiles)
formData.append(file.name, file);
console.debug(DEBUG_PREFIX, "Sending files:", formData);
url.pathname = '/api/voice-conversion/rvc/upload-models';
const apiResult = await doExtrasFetch(url, {
method: 'POST',
body: formData
});
if (!apiResult.ok) {
toastr.error(apiResult.statusText, DEBUG_PREFIX + ' Check extras console for errors log');
throw new Error(`HTTP ${apiResult.status}: ${await apiResult.text()}`);
}
alert('The files have been uploaded successfully.');
}
$(document).ready(function () {
function addExtensionControls() {
const settingsHtml = `
<div id="rvc_settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>RVC</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<h4 class="center">Characters Voice Mapping</h4>
<div>
<label class="checkbox_label" for="rvc_enabled">
<input type="checkbox" id="rvc_enabled" name="rvc_enabled">
<small>Enabled</small>
</label>
<label>Voice Map (debug infos)</label>
<textarea id="rvc_voice_map" type="text" class="text_pole textarea_compact" rows="4"
placeholder="Voice map will appear here for debug purpose"></textarea>
</div>
<div>
<div class="background_controls">
<label for="rvc_character_select">Character:</label>
<select id="rvc_character_select">
<!-- Populated by JS -->
</select>
<div id="rvc_delete" class="menu_button">
<i class="fa-solid fa-times"></i>
Remove
</div>
</div>
<div class="background_controls">
<label for="rvc_model_select">Voice:</label>
<select id="rvc_model_select">
<!-- Populated by JS -->
</select>
<div id="rvc_model_refresh_button" class="menu_button">
<i class="fa-solid fa-refresh"></i>
<!-- Refresh -->
</div>
<div id="rvc_model_upload_select_button" class="menu_button">
<i class="fa-solid fa-upload"></i>
Upload
</div>
<input
type="file"
id="rvc_model_upload_files"
accept=".zip,.rar,.7zip,.7z" multiple />
</div>
</div>
<div>
<small>
Upload one archive per model. With .pth and .index (optional) inside.<br/>
Supported format: .zip .rar .7zip .7z
</small>
</div>
<div>
<h4>Model Settings</h4>
</div>
<div>
<label for="rvc_pitch_extraction">
Pitch Extraction
</label>
<select id="rvc_pitch_extraction">
<option value="dio">dio</option>
<option value="pm">pm</option>
<option value="harvest">harvest</option>
<option value="torchcrepe">torchcrepe</option>
<option value="rmvpe">rmvpe</option>
<option value="">None</option>
</select>
<small>
Tips: dio and pm faster, harvest slower but good.<br/>
Torchcrepe and rmvpe are good but uses GPU.
</small>
</div>
<div>
<label for="rvc_index_rate">
Search feature ratio (<span id="rvc_index_rate_value"></span>)
</label>
<input id="rvc_index_rate" type="range" min="0" max="1" step="0.01" value="0.5" />
<small>
Controls accent strength, too high may produce artifact.
</small>
</div>
<div>
<label for="rvc_filter_radius">Filter radius (<span id="rvc_filter_radius_value"></span>)</label>
<input id="rvc_filter_radius" type="range" min="0" max="7" step="1" value="3" />
<small>
Higher can reduce breathiness but may increase run time.
</small>
</div>
<div>
<label for="rvc_pitch_offset">Pitch offset (<span id="rvc_pitch_offset_value"></span>)</label>
<input id="rvc_pitch_offset" type="range" min="-20" max="20" step="1" value="0" />
<small>
Recommended +12 key for male to female conversion and -12 key for female to male conversion.
</small>
</div>
<div>
<label for="rvc_rms_mix_rate">Mix rate (<span id="rvc_rms_mix_rate_value"></span>)</label>
<input id="rvc_rms_mix_rate" type="range" min="0" max="1" step="0.01" value="1" />
<small>
Closer to 0 is closer to TTS and 1 is closer to trained voice.
Can help mask noise and sound more natural when set relatively low.
</small>
</div>
<div>
<label for="rvc_protect">Protect amount (<span id="rvc_protect_value"></span>)</label>
<input id="rvc_protect" type="range" min="0" max="1" step="0.01" value="0.33" />
<small>
Avoid non voice sounds. Lower is more being ignored.
</small>
</div>
<div id="rvc_status">
</div>
<div class="rvc_buttons">
<input id="rvc_apply" class="menu_button" type="submit" value="Apply" />
</div>
</div>
</div>
</div>
</div>
`;
$('#extensions_settings').append(settingsHtml);
$("#rvc_enabled").on("click", onEnabledClick);
$("#rvc_voice_map").attr("disabled", "disabled");;
$('#rvc_pitch_extraction').on('change', onPitchExtractionChange);
$('#rvc_index_rate').on('input', onIndexRateChange);
$('#rvc_filter_radius').on('input', onFilterRadiusChange);
$('#rvc_pitch_offset').on('input', onPitchOffsetChange);
$('#rvc_rms_mix_rate').on('input', onRmsMixRateChange);
$('#rvc_protect').on('input', onProtectChange);
$("#rvc_apply").on("click", onApplyClick);
$("#rvc_delete").on("click", onDeleteClick);
$("#rvc_model_upload_files").hide();
$("#rvc_model_upload_select_button").on("click", function() {$("#rvc_model_upload_files").click()});
$("#rvc_model_upload_files").on("change", onChangeUploadFiles);
//$("#rvc_model_upload_button").on("click", onClickUpload);
$("#rvc_model_refresh_button").on("click", refreshVoiceList);
}
addExtensionControls(); // No init dependencies
loadSettings(); // Depends on Extension Controls
const wrapper = new ModuleWorkerWrapper(moduleWorker);
setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL);
moduleWorker();
})
//#############################//
// API Calls //
//#############################//
/*
Check model installation state, return one of ["installed", "corrupted", "absent"]
*/
async function get_models_list(model_id) {
const url = new URL(getApiUrl());
url.pathname = '/api/voice-conversion/rvc/get-models-list';
const apiResult = await doExtrasFetch(url, {
method: 'POST'
});
if (!apiResult.ok) {
toastr.error(apiResult.statusText, DEBUG_PREFIX + ' Check model state request failed');
throw new Error(`HTTP ${apiResult.status}: ${await apiResult.text()}`);
}
return apiResult
}
/*
Send an audio file to RVC to convert voice
*/
async function rvcVoiceConversion(response, character, text) {
let apiResult
// Check voice map
if (extension_settings.rvc.voiceMap[character] === undefined) {
//toastr.error("No model is assigned to character '"+character+"', check RVC voice map in the extension menu.", DEBUG_PREFIX+'RVC Voice map error', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
console.info(DEBUG_PREFIX, "No RVC model assign in voice map for current character " + character);
return response;
}
const audioData = await response.blob()
if (!audioData.type in ['audio/mpeg', 'audio/wav', 'audio/x-wav', 'audio/wave', 'audio/webm']) {
throw `TTS received HTTP response with invalid data format. Expecting audio/mpeg, got ${audioData.type}`
}
console.log("Audio type received:", audioData.type)
const voice_settings = extension_settings.rvc.voiceMap[character];
var requestData = new FormData();
requestData.append('AudioFile', audioData, 'record');
requestData.append("json", JSON.stringify({
"modelName": voice_settings["modelName"],
"pitchExtraction": voice_settings["pitchExtraction"],
"pitchOffset": voice_settings["pitchOffset"],
"indexRate": voice_settings["indexRate"],
"filterRadius": voice_settings["filterRadius"],
"rmsMixRate": voice_settings["rmsMixRate"],
"protect": voice_settings["protect"],
"text": text
}));
console.log("Sending tts audio data to RVC on extras server",requestData)
const url = new URL(getApiUrl());
url.pathname = '/api/voice-conversion/rvc/process-audio';
apiResult = await doExtrasFetch(url, {
method: 'POST',
body: requestData,
});
if (!apiResult.ok) {
toastr.error(apiResult.statusText, DEBUG_PREFIX + ' RVC Voice Conversion Failed', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
throw new Error(`HTTP ${apiResult.status}: ${await apiResult.text()}`);
}
return apiResult;
}
//#############################//
// Module Worker //
//#############################//
async function refreshVoiceList() {
let result = await get_models_list();
result = await result.json();
rvcModelsList = result["models_list"]
$('#rvc_model_select')
.find('option')
.remove()
.end()
.append('<option value="none">Select Voice</option>')
.val('none')
for (const modelName of rvcModelsList) {
$("#rvc_model_select").append(new Option(modelName, modelName));
}
rvcModelsReceived = true
console.debug(DEBUG_PREFIX, "Updated model list to:", rvcModelsList);
}
async function moduleWorker() {
updateCharactersList();
if (modules.includes('rvc') && !rvcModelsReceived) {
refreshVoiceList();
}
}
function updateCharactersList() {
let currentcharacters = new Set();
const context = getContext();
for (const i of context.characters) {
currentcharacters.add(i.name);
}
currentcharacters = Array.from(currentcharacters);
currentcharacters.unshift(context.name1);
if (JSON.stringify(charactersList) !== JSON.stringify(currentcharacters)) {
charactersList = currentcharacters
$('#rvc_character_select')
.find('option')
.remove()
.end()
.append('<option value="none">Select Character</option>')
.val('none')
for (const charName of charactersList) {
$("#rvc_character_select").append(new Option(charName, charName));
}
console.debug(DEBUG_PREFIX, "Updated character list to:", charactersList);
}
}

View File

@@ -1,11 +0,0 @@
{
"display_name": "RVC",
"loading_order": 13,
"requires": ["rvc"],
"optional": [],
"js": "index.js",
"css": "style.css",
"author": "Keij#6799",
"version": "0.1.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
}

View File

@@ -1,3 +0,0 @@
.speech-toggle {
display: flex;
}

View File

@@ -1,11 +0,0 @@
{
"display_name": "Settings Search",
"loading_order": 15,
"requires": [],
"optional": [],
"js": "index.js",
"css": "style.css",
"author": "RossAscends",
"version": "1.0.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
}

View File

@@ -1,5 +0,0 @@
.highlighted {
color: black;
background-color: yellow;
text-shadow: none !important;
}

View File

@@ -1,233 +0,0 @@
// Borrowed from Agnai (AGPLv3)
// https://github.com/agnaistic/agnai/blob/dev/web/pages/Chat/components/SpeechRecognitionRecorder.tsx
// First version by Cohee#1207
// Adapted by Tony-sama
export { BrowserSttProvider }
const DEBUG_PREFIX = "<Speech Recognition module (Browser)> "
class BrowserSttProvider {
//########//
// Config //
//########//
settings = {
language: ""
}
defaultSettings = {
language: "en-US",
}
processTranscriptFunction = null;
get settingsHtml() {
let html = ' \
<span>Language</span> </br> \
<select id="speech_recognition_browser_provider_language"> \
<option value="ar-SA">ar-SA: Arabic (Saudi Arabia)</option> \
<option value="bn-BD">bn-BD: Bangla (Bangladesh)</option> \
<option value="bn-IN">bn-IN: Bangla (India)</option> \
<option value="cs-CZ">cs-CZ: Czech (Czech Republic)</option> \
<option value="da-DK">da-DK: Danish (Denmark)</option> \
<option value="de-AT">de-AT: German (Austria)</option> \
<option value="de-CH">de-CH: German (Switzerland)</option> \
<option value="de-DE">de-DE: German (Germany)</option> \
<option value="el-GR">el-GR: Greek (Greece)</option> \
<option value="en-AU">en-AU: English (Australia)</option> \
<option value="en-CA">en-CA: English (Canada)</option> \
<option value="en-GB">en-GB: English (United Kingdom)</option> \
<option value="en-IE">en-IE: English (Ireland)</option> \
<option value="en-IN">en-IN: English (India)</option> \
<option value="en-NZ">en-NZ: English (New Zealand)</option> \
<option value="en-US">en-US: English (United States)</option> \
<option value="en-ZA">en-ZA: English (South Africa)</option> \
<option value="es-AR">es-AR: Spanish (Argentina)</option> \
<option value="es-CL">es-CL: Spanish (Chile)</option> \
<option value="es-CO">es-CO: Spanish (Columbia)</option> \
<option value="es-ES">es-ES: Spanish (Spain)</option> \
<option value="es-MX">es-MX: Spanish (Mexico)</option> \
<option value="es-US">es-US: Spanish (United States)</option> \
<option value="fi-FI">fi-FI: Finnish (Finland)</option> \
<option value="fr-BE">fr-BE: French (Belgium)</option> \
<option value="fr-CA">fr-CA: French (Canada)</option> \
<option value="fr-CH">fr-CH: French (Switzerland)</option> \
<option value="fr-FR">fr-FR: French (France)</option> \
<option value="he-IL">he-IL: Hebrew (Israel)</option> \
<option value="hi-IN">hi-IN: Hindi (India)</option> \
<option value="hu-HU">hu-HU: Hungarian (Hungary)</option> \
<option value="id-ID">id-ID: Indonesian (Indonesia)</option> \
<option value="it-CH">it-CH: Italian (Switzerland)</option> \
<option value="it-IT">it-IT: Italian (Italy)</option> \
<option value="ja-JP">ja-JP: Japanese (Japan)</option> \
<option value="ko-KR">ko-KR: Korean (Republic of Korea)</option> \
<option value="nl-BE">nl-BE: Dutch (Belgium)</option> \
<option value="nl-NL">nl-NL: Dutch (The Netherlands)</option> \
<option value="no-NO">no-NO: Norwegian (Norway)</option> \
<option value="pl-PL">pl-PL: Polish (Poland)</option> \
<option value="pt-BR">pt-BR: Portugese (Brazil)</option> \
<option value="pt-PT">pt-PT: Portugese (Portugal)</option> \
<option value="ro-RO">ro-RO: Romanian (Romania)</option> \
<option value="ru-RU">ru-RU: Russian (Russian Federation)</option> \
<option value="sk-SK">sk-SK: Slovak (Slovakia)</option> \
<option value="sv-SE">sv-SE: Swedish (Sweden)</option> \
<option value="ta-IN">ta-IN: Tamil (India)</option> \
<option value="ta-LK">ta-LK: Tamil (Sri Lanka)</option> \
<option value="th-TH">th-TH: Thai (Thailand)</option> \
<option value="tr-TR">tr-TR: Turkish (Turkey)</option> \
<option value="zh-CN">zh-CN: Chinese (China)</option> \
<option value="zh-HK">zh-HK: Chinese (Hond Kong)</option> \
<option value="zh-TW">zh-TW: Chinese (Taiwan)</option> \
</select> \
'
return html
}
onSettingsChange() {
// Used when provider settings are updated from UI
this.settings.language = $("#speech_recognition_browser_provider_language").val();
console.debug(DEBUG_PREFIX+"Change language to",this.settings.language);
this.loadSettings(this.settings);
}
static capitalizeInterim(interimTranscript) {
let capitalizeIndex = -1;
if (interimTranscript.length > 2 && interimTranscript[0] === ' ') capitalizeIndex = 1;
else if (interimTranscript.length > 1) capitalizeIndex = 0;
if (capitalizeIndex > -1) {
const spacing = capitalizeIndex > 0 ? ' '.repeat(capitalizeIndex - 1) : '';
const capitalized = interimTranscript[capitalizeIndex].toLocaleUpperCase();
const rest = interimTranscript.substring(capitalizeIndex + 1);
interimTranscript = spacing + capitalized + rest;
}
return interimTranscript;
}
static composeValues(previous, interim) {
let spacing = '';
if (previous.endsWith('.')) spacing = ' ';
return previous + spacing + interim;
}
loadSettings(settings) {
const processTranscript = this.processTranscriptFunction;
// Populate Provider UI given input settings
if (Object.keys(settings).length == 0) {
console.debug(DEBUG_PREFIX+"Using default browser STT settings")
}
// Initialise as defaultSettings
this.settings = this.defaultSettings;
for (const key in settings){
if (key in this.settings){
this.settings[key] = settings[key]
} else {
throw `Invalid setting passed to Speech recogniton extension (browser): ${key}`
}
}
$("#speech_recognition_browser_provider_language").val(this.settings.language);
const speechRecognitionSettings = $.extend({
grammar: '' // Custom grammar
}, options);
const speechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
const speechRecognitionList = window.SpeechGrammarList || window.webkitSpeechGrammarList;
if (!speechRecognition) {
console.warn(DEBUG_PREFIX+'Speech recognition is not supported in this browser.');
$("#microphone_button").hide();
toastr.error("Speech recognition is not supported in this browser, use another browser or another provider of SillyTavern-extras Speech recognition extension.", "Speech recognition activation Failed (Browser)", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
return;
}
const recognition = new speechRecognition();
if (speechRecognitionSettings.grammar && speechRecognitionList) {
speechRecognitionList.addFromString(speechRecognitionSettings.grammar, 1);
recognition.grammars = speechRecognitionList;
}
recognition.continuous = true;
recognition.interimResults = true;
recognition.lang = this.settings.language;
const textarea = $('#send_textarea');
const button = $('#microphone_button');
let listening = false;
button.off('click').on("click", function () {
if (listening) {
recognition.stop();
} else {
recognition.start();
}
listening = !listening;
});
let initialText = '';
recognition.onresult = function (speechEvent) {
let finalTranscript = '';
let interimTranscript = ''
for (let i = speechEvent.resultIndex; i < speechEvent.results.length; ++i) {
const transcript = speechEvent.results[i][0].transcript;
if (speechEvent.results[i].isFinal) {
let interim = BrowserSttProvider.capitalizeInterim(transcript);
if (interim != '') {
let final = finalTranscript;
final = BrowserSttProvider.composeValues(final, interim);
if (final.slice(-1) != '.' & final.slice(-1) != '?') final += '.';
finalTranscript = final;
recognition.abort();
listening = false;
}
interimTranscript = ' ';
} else {
interimTranscript += transcript;
}
}
interimTranscript = BrowserSttProvider.capitalizeInterim(interimTranscript);
textarea.val(initialText + finalTranscript + interimTranscript);
};
recognition.onerror = function (event) {
console.error('Error occurred in recognition:', event.error);
//if ($('#speech_recognition_debug').is(':checked'))
// toastr.error('Error occurred in recognition:'+ event.error, 'STT Generation error (Browser)', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
};
recognition.onend = function () {
listening = false;
button.toggleClass('fa-microphone fa-microphone-slash');
const newText = textarea.val().substring(initialText.length);
textarea.val(textarea.val().substring(0,initialText.length));
processTranscript(newText);
};
recognition.onstart = function () {
initialText = textarea.val();
button.toggleClass('fa-microphone fa-microphone-slash');
if ($("#speech_recognition_message_mode").val() == "replace") {
textarea.val("");
initialText = ""
}
};
$("#microphone_button").show();
console.debug(DEBUG_PREFIX+"Browser STT settings loaded")
}
}

View File

@@ -1,452 +0,0 @@
/*
TODO:
- try pseudo streaming audio by just sending chunk every X seconds and asking VOSK if it is full text.
*/
import { saveSettingsDebounced } from "../../../script.js";
import { getContext, extension_settings, ModuleWorkerWrapper } from "../../extensions.js";
import { VoskSttProvider } from './vosk.js'
import { WhisperSttProvider } from './whisper.js'
import { BrowserSttProvider } from './browser.js'
import { StreamingSttProvider } from './streaming.js'
import { getMessageTimeStamp } from "../../RossAscends-mods.js";
export { MODULE_NAME };
const MODULE_NAME = 'Speech Recognition';
const DEBUG_PREFIX = "<Speech Recognition module> "
const UPDATE_INTERVAL = 100;
let inApiCall = false;
let sttProviders = {
None: null,
Browser: BrowserSttProvider,
Whisper: WhisperSttProvider,
Vosk: VoskSttProvider,
Streaming: StreamingSttProvider,
}
let sttProvider = null
let sttProviderName = "None"
let audioRecording = false
const constraints = { audio: { sampleSize: 16, channelCount: 1, sampleRate: 16000 } };
let audioChunks = [];
async function moduleWorker() {
if (sttProviderName != "Streaming") {
return;
}
// API is busy
if (inApiCall) {
return;
}
try {
inApiCall = true;
const userMessageOriginal = await sttProvider.getUserMessage();
let userMessageFormatted = userMessageOriginal.trim();
if (userMessageFormatted.length > 0)
{
console.debug(DEBUG_PREFIX+"recorded transcript: \""+userMessageFormatted+"\"");
let userMessageLower = userMessageFormatted.toLowerCase();
// remove punctuation
let userMessageRaw = userMessageLower.replace(/[^\w\s\']|_/g, "").replace(/\s+/g, " ");
console.debug(DEBUG_PREFIX+"raw transcript:",userMessageRaw);
// Detect trigger words
let messageStart = -1;
if (extension_settings.speech_recognition.Streaming.triggerWordsEnabled) {
for (const triggerWord of extension_settings.speech_recognition.Streaming.triggerWords) {
const triggerPos = userMessageRaw.indexOf(triggerWord.toLowerCase());
// Trigger word not found or not starting message and just a substring
if (triggerPos == -1){ // | (triggerPos > 0 & userMessageFormatted[triggerPos-1] != " ")) {
console.debug(DEBUG_PREFIX+"trigger word not found: ", triggerWord);
}
else {
console.debug(DEBUG_PREFIX+"Found trigger word: ", triggerWord, " at index ", triggerPos);
if (triggerPos < messageStart || messageStart == -1) { // & (triggerPos + triggerWord.length) < userMessageFormatted.length)) {
messageStart = triggerPos; // + triggerWord.length + 1;
if (!extension_settings.speech_recognition.Streaming.triggerWordsIncluded)
messageStart = triggerPos + triggerWord.length + 1;
}
}
}
} else {
messageStart = 0;
}
if (messageStart == -1) {
console.debug(DEBUG_PREFIX+"message ignored, no trigger word preceding a message. Voice transcript: \""+ userMessageOriginal +"\"");
if (extension_settings.speech_recognition.Streaming.debug) {
toastr.info(
"No trigger word preceding a message. Voice transcript: \""+ userMessageOriginal +"\"",
DEBUG_PREFIX+"message ignored.",
{ timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true },
);
}
}
else{
userMessageFormatted = userMessageFormatted.substring(messageStart);
// Trim non alphanumeric character from the start
messageStart = 0;
for(const i of userMessageFormatted) {
if(/^[a-z]$/i.test(i)) {
break;
}
messageStart += 1;
}
userMessageFormatted = userMessageFormatted.substring(messageStart);
userMessageFormatted = userMessageFormatted.charAt(0).toUpperCase() + userMessageFormatted.substring(1);
processTranscript(userMessageFormatted);
}
}
else
{
console.debug(DEBUG_PREFIX+"Received empty transcript, ignored");
}
}
catch (error) {
console.debug(error);
}
finally {
inApiCall = false;
}
}
async function processTranscript(transcript) {
try {
const transcriptOriginal = transcript;
let transcriptFormatted = transcriptOriginal.trim();
if (transcriptFormatted.length > 0)
{
console.debug(DEBUG_PREFIX+"recorded transcript: \""+transcriptFormatted+"\"");
const messageMode = extension_settings.speech_recognition.messageMode;
console.debug(DEBUG_PREFIX+"mode: "+messageMode);
let transcriptLower = transcriptFormatted.toLowerCase()
// remove punctuation
let transcriptRaw = transcriptLower.replace(/[^\w\s\']|_/g, "").replace(/\s+/g, " ");
// Check message mapping
if (extension_settings.speech_recognition.messageMappingEnabled) {
console.debug(DEBUG_PREFIX+"Start searching message mapping into:",transcriptRaw)
for (const key in extension_settings.speech_recognition.messageMapping) {
console.debug(DEBUG_PREFIX+"message mapping searching: ", key,"=>",extension_settings.speech_recognition.messageMapping[key]);
if (transcriptRaw.includes(key)) {
var message = extension_settings.speech_recognition.messageMapping[key];
console.debug(DEBUG_PREFIX+"message mapping found: ", key,"=>",extension_settings.speech_recognition.messageMapping[key]);
$("#send_textarea").val(message);
if (messageMode == "auto_send") await getContext().generate();
return;
}
}
}
console.debug(DEBUG_PREFIX+"no message mapping found, processing transcript as normal message");
switch (messageMode) {
case "auto_send":
$('#send_textarea').val("") // clear message area to avoid double message
console.debug(DEBUG_PREFIX+"Sending message")
const context = getContext();
const messageText = transcriptFormatted;
const message = {
name: context.name1,
is_user: true,
send_date: getMessageTimeStamp(),
mes: messageText,
};
context.chat.push(message);
context.addOneMessage(message);
await context.generate();
$('#debug_output').text("<SST-module DEBUG>: message sent: \""+ transcriptFormatted +"\"");
break;
case "replace":
console.debug(DEBUG_PREFIX+"Replacing message")
$('#send_textarea').val(transcriptFormatted);
break;
case "append":
console.debug(DEBUG_PREFIX+"Appending message")
$('#send_textarea').val($('#send_textarea').val()+" "+transcriptFormatted);
break;
default:
console.debug(DEBUG_PREFIX+"Not supported stt message mode: "+messageMode)
}
}
else
{
console.debug(DEBUG_PREFIX+"Empty transcript, do nothing");
}
}
catch (error) {
console.debug(error);
}
}
function loadNavigatorAudioRecording() {
if (navigator.mediaDevices.getUserMedia) {
console.debug(DEBUG_PREFIX+' getUserMedia supported by browser.');
let onSuccess = function(stream) {
const mediaRecorder = new MediaRecorder(stream);
$("#microphone_button").off('click').on("click", function() {
if (!audioRecording) {
mediaRecorder.start();
console.debug(mediaRecorder.state);
console.debug("recorder started");
audioRecording = true;
$("#microphone_button").toggleClass('fa-microphone fa-microphone-slash');
}
else {
mediaRecorder.stop();
console.debug(mediaRecorder.state);
console.debug("recorder stopped");
audioRecording = false;
$("#microphone_button").toggleClass('fa-microphone fa-microphone-slash');
}
});
mediaRecorder.onstop = async function() {
console.debug(DEBUG_PREFIX+"data available after MediaRecorder.stop() called: ", audioChunks.length, " chunks");
const audioBlob = new Blob(audioChunks, { type: "audio/wav; codecs=0" });
audioChunks = [];
const transcript = await sttProvider.processAudio(audioBlob);
// TODO: lock and release recording while processing?
console.debug(DEBUG_PREFIX+"received transcript:", transcript);
processTranscript(transcript);
}
mediaRecorder.ondataavailable = function(e) {
audioChunks.push(e.data);
}
}
let onError = function(err) {
console.debug(DEBUG_PREFIX+"The following error occured: " + err);
}
navigator.mediaDevices.getUserMedia(constraints).then(onSuccess, onError);
} else {
console.debug(DEBUG_PREFIX+"getUserMedia not supported on your browser!");
toastr.error("getUserMedia not supported", DEBUG_PREFIX+"not supported for your browser.", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
}
}
//##############//
// STT Provider //
//##############//
function loadSttProvider(provider) {
//Clear the current config and add new config
$("#speech_recognition_provider_settings").html("");
// Init provider references
extension_settings.speech_recognition.currentProvider = provider;
sttProviderName = provider;
if (!(sttProviderName in extension_settings.speech_recognition)) {
console.warn(`Provider ${sttProviderName} not in Extension Settings, initiatilizing provider in settings`);
extension_settings.speech_recognition[sttProviderName] = {};
}
$('#speech_recognition_provider').val(sttProviderName);
if (sttProviderName == "None") {
$("#microphone_button").hide();
$("#speech_recognition_message_mode_div").hide();
$("#speech_recognition_message_mapping_div").hide();
return;
}
$("#speech_recognition_message_mode_div").show();
$("#speech_recognition_message_mapping_div").show();
sttProvider = new sttProviders[sttProviderName]
// Init provider settings
$('#speech_recognition_provider_settings').append(sttProvider.settingsHtml);
// Use microphone button as push to talk
if (sttProviderName == "Browser") {
sttProvider.processTranscriptFunction = processTranscript;
sttProvider.loadSettings(extension_settings.speech_recognition[sttProviderName]);
$("#microphone_button").show();
}
if (sttProviderName == "Vosk" | sttProviderName == "Whisper") {
sttProvider.loadSettings(extension_settings.speech_recognition[sttProviderName]);
loadNavigatorAudioRecording();
$("#microphone_button").show();
}
if (sttProviderName == "Streaming") {
sttProvider.loadSettings(extension_settings.speech_recognition[sttProviderName]);
$("#microphone_button").off('click');
$("#microphone_button").hide();
}
}
function onSttProviderChange() {
const sttProviderSelection = $('#speech_recognition_provider').val();
loadSttProvider(sttProviderSelection);
saveSettingsDebounced();
}
function onSttProviderSettingsInput() {
sttProvider.onSettingsChange();
// Persist changes to SillyTavern stt extension settings
extension_settings.speech_recognition[sttProviderName] = sttProvider.settings;
saveSettingsDebounced();
console.info(`Saved settings ${sttProviderName} ${JSON.stringify(sttProvider.settings)}`);
}
//#############################//
// Extension UI and Settings //
//#############################//
const defaultSettings = {
currentProvider: "None",
messageMode: "append",
messageMappingText: "",
messageMapping: [],
messageMappingEnabled: false,
}
function loadSettings() {
if (Object.keys(extension_settings.speech_recognition).length === 0) {
Object.assign(extension_settings.speech_recognition, defaultSettings)
}
$('#speech_recognition_enabled').prop('checked',extension_settings.speech_recognition.enabled);
$('#speech_recognition_message_mode').val(extension_settings.speech_recognition.messageMode);
if (extension_settings.speech_recognition.messageMappingText.length > 0) {
$('#speech_recognition_message_mapping').val(extension_settings.speech_recognition.messageMappingText);
}
$('#speech_recognition_message_mapping_enabled').prop('checked',extension_settings.speech_recognition.messageMappingEnabled);
}
async function onMessageModeChange() {
extension_settings.speech_recognition.messageMode = $('#speech_recognition_message_mode').val();
if(sttProviderName != "Browser" & extension_settings.speech_recognition.messageMode == "auto_send") {
$("#speech_recognition_wait_response_div").show()
}
else {
$("#speech_recognition_wait_response_div").hide()
}
saveSettingsDebounced();
}
async function onMessageMappingChange() {
let array = $('#speech_recognition_message_mapping').val().split(",");
array = array.map(element => {return element.trim();});
array = array.filter((str) => str !== '');
extension_settings.speech_recognition.messageMapping = {};
for (const text of array) {
if (text.includes("=")) {
const pair = text.toLowerCase().split("=")
extension_settings.speech_recognition.messageMapping[pair[0].trim()] = pair[1].trim()
console.debug(DEBUG_PREFIX+"Added mapping", pair[0],"=>", extension_settings.speech_recognition.messageMapping[pair[0]]);
}
else {
console.debug(DEBUG_PREFIX+"Wrong syntax for message mapping, no '=' found in:", text);
}
}
$("#speech_recognition_message_mapping_status").text("Message mapping updated to: "+JSON.stringify(extension_settings.speech_recognition.messageMapping))
console.debug(DEBUG_PREFIX+"Updated message mapping", extension_settings.speech_recognition.messageMapping);
extension_settings.speech_recognition.messageMappingText = $('#speech_recognition_message_mapping').val()
saveSettingsDebounced();
}
async function onMessageMappingEnabledClick() {
extension_settings.speech_recognition.messageMappingEnabled = $('#speech_recognition_message_mapping_enabled').is(':checked');
saveSettingsDebounced()
}
$(document).ready(function () {
function addExtensionControls() {
const settingsHtml = `
<div id="speech_recognition_settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>Speech Recognition</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<div>
<span>Select Speech-to-text Provider</span> </br>
<select id="speech_recognition_provider">
</select>
</div>
<div id="speech_recognition_message_mode_div">
<span>Message Mode</span> </br>
<select id="speech_recognition_message_mode">
<option value="append">Append</option>
<option value="replace">Replace</option>
<option value="auto_send">Auto send</option>
</select>
</div>
<div id="speech_recognition_message_mapping_div">
<span>Message Mapping</span>
<textarea id="speech_recognition_message_mapping" class="text_pole textarea_compact" type="text" rows="4" placeholder="Enter comma separated phrases mapping, example:\ncommand delete = /del 2,\nslash delete = /del 2,\nsystem roll = /roll 2d6,\nhey continue = /continue"></textarea>
<span id="speech_recognition_message_mapping_status"></span>
<label class="checkbox_label" for="speech_recognition_message_mapping_enabled">
<input type="checkbox" id="speech_recognition_message_mapping_enabled" name="speech_recognition_message_mapping_enabled">
<small>Enable messages mapping</small>
</label>
</div>
<form id="speech_recognition_provider_settings" class="inline-drawer-content">
</form>
</div>
</div>
</div>
`;
$('#extensions_settings').append(settingsHtml);
$('#speech_recognition_provider_settings').on('input', onSttProviderSettingsInput);
for (const provider in sttProviders) {
$('#speech_recognition_provider').append($("<option />").val(provider).text(provider));
console.debug(DEBUG_PREFIX+"added option "+provider);
}
$('#speech_recognition_provider').on('change', onSttProviderChange);
$('#speech_recognition_message_mode').on('change', onMessageModeChange);
$('#speech_recognition_message_mapping').on('change', onMessageMappingChange);
$('#speech_recognition_message_mapping_enabled').on('click', onMessageMappingEnabledClick);
const $button = $('<div id="microphone_button" class="fa-solid fa-microphone speech-toggle" title="Click to speak"></div>');
$('#send_but_sheld').prepend($button);
}
addExtensionControls(); // No init dependencies
loadSettings(); // Depends on Extension Controls and loadTtsProvider
loadSttProvider(extension_settings.speech_recognition.currentProvider); // No dependencies
const wrapper = new ModuleWorkerWrapper(moduleWorker);
setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL); // Init depends on all the things
moduleWorker();
})

View File

@@ -1,14 +0,0 @@
{
"display_name": "Speech Recognition",
"loading_order": 13,
"requires": [],
"optional": [
"vosk-speech-recognition",
"whisper-speech-recognition"
],
"js": "index.js",
"css": "style.css",
"author": "Cohee#1207 and Keij#6799",
"version": "1.1.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
}

View File

@@ -1,109 +0,0 @@
import { getApiUrl, doExtrasFetch, modules } from "../../extensions.js";
export { StreamingSttProvider }
const DEBUG_PREFIX = "<Speech Recognition module (streaming)> "
class StreamingSttProvider {
//########//
// Config //
//########//
settings
defaultSettings = {
triggerWordsText: "",
triggerWords : [],
triggerWordsEnabled : false,
triggerWordsIncluded: false,
debug : false,
}
get settingsHtml() {
let html = '\
<div id="speech_recognition_streaming_trigger_words_div">\
<span>Trigger words</span>\
<textarea id="speech_recognition_streaming_trigger_words" class="text_pole textarea_compact" type="text" rows="4" placeholder="Enter comma separated words that triggers new message, example:\nhey, hey aqua, record, listen"></textarea>\
<label class="checkbox_label" for="speech_recognition_streaming_trigger_words_enabled">\
<input type="checkbox" id="speech_recognition_streaming_trigger_words_enabled" name="speech_recognition_trigger_words_enabled">\
<small>Enable trigger words</small>\
</label>\
<label class="checkbox_label" for="speech_recognition_trigger_words_included">\
<input type="checkbox" id="speech_recognition_trigger_words_included" name="speech_recognition_trigger_words_included">\
<small>Include trigger words in message</small>\
</label>\
<label class="checkbox_label" for="speech_recognition_streaming_debug">\
<input type="checkbox" id="speech_recognition_streaming_debug" name="speech_recognition_streaming_debug">\
<small>Enable debug pop ups</small>\
</label>\
</div>\
'
return html
}
onSettingsChange() {
this.settings.triggerWordsText = $('#speech_recognition_streaming_trigger_words').val();
let array = $('#speech_recognition_streaming_trigger_words').val().split(",");
array = array.map(element => {return element.trim().toLowerCase();});
array = array.filter((str) => str !== '');
this.settings.triggerWords = array;
this.settings.triggerWordsEnabled = $("#speech_recognition_streaming_trigger_words_enabled").is(':checked');
this.settings.triggerWordsIncluded = $("#speech_recognition_trigger_words_included").is(':checked');
this.settings.debug = $("#speech_recognition_streaming_debug").is(':checked');
console.debug(DEBUG_PREFIX+" Updated settings: ", this.settings);
this.loadSettings(this.settings);
}
loadSettings(settings) {
// Populate Provider UI given input settings
if (Object.keys(settings).length == 0) {
console.debug(DEBUG_PREFIX+"Using default Whisper STT extension settings")
}
// Only accept keys defined in defaultSettings
this.settings = this.defaultSettings
for (const key in settings){
if (key in this.settings){
this.settings[key] = settings[key]
} else {
throw `Invalid setting passed to STT extension: ${key}`
}
}
$("#speech_recognition_streaming_trigger_words").val(this.settings.triggerWordsText);
$("#speech_recognition_streaming_trigger_words_enabled").prop('checked',this.settings.triggerWordsEnabled);
$("#speech_recognition_trigger_words_included").prop('checked',this.settings.triggerWordsIncluded);
$("#speech_recognition_streaming_debug").prop('checked',this.settings.debug);
console.debug(DEBUG_PREFIX+"streaming STT settings loaded")
}
async getUserMessage() {
// Return if module is not loaded
if (!modules.includes('streaming-stt')) {
console.debug(DEBUG_PREFIX+"Module streaming-stt must be activated in Sillytavern Extras for streaming user voice.")
return "";
}
const url = new URL(getApiUrl());
url.pathname = '/api/speech-recognition/streaming/record-and-transcript';
const apiResult = await doExtrasFetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Bypass-Tunnel-Reminder': 'bypass',
},
body: JSON.stringify({ text: "" }),
});
if (!apiResult.ok) {
toastr.error(apiResult.statusText, DEBUG_PREFIX+'STT Generation Failed (streaming)', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
throw new Error(`HTTP ${apiResult.status}: ${await apiResult.text()}`);
}
const data = await apiResult.json();
return data.transcript;
}
}

View File

@@ -1,3 +0,0 @@
.speech-toggle {
display: flex;
}

View File

@@ -1,65 +0,0 @@
import { getApiUrl, doExtrasFetch } from "../../extensions.js";
export { VoskSttProvider }
const DEBUG_PREFIX = "<Speech Recognition module (Vosk)> "
class VoskSttProvider {
//########//
// Config //
//########//
settings
defaultSettings = {
}
get settingsHtml() {
let html = ""
return html
}
onSettingsChange() {
// Used when provider settings are updated from UI
}
loadSettings(settings) {
// Populate Provider UI given input settings
if (Object.keys(settings).length == 0) {
console.debug(DEBUG_PREFIX+"Using default vosk STT extension settings")
}
// Only accept keys defined in defaultSettings
this.settings = this.defaultSettings
for (const key in settings){
if (key in this.settings){
this.settings[key] = settings[key]
} else {
throw `Invalid setting passed to STT extension: ${key}`
}
}
console.debug(DEBUG_PREFIX+"Vosk STT settings loaded")
}
async processAudio(audioblob) {
var requestData = new FormData();
requestData.append('AudioFile', audioblob, 'record.wav');
const url = new URL(getApiUrl());
url.pathname = '/api/speech-recognition/vosk/process-audio';
const apiResult = await doExtrasFetch(url, {
method: 'POST',
body: requestData,
});
if (!apiResult.ok) {
toastr.error(apiResult.statusText, 'STT Generation Failed (Vosk)', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
throw new Error(`HTTP ${apiResult.status}: ${await apiResult.text()}`);
}
const result = await apiResult.json();
return result.transcript;
}
}

View File

@@ -1,67 +0,0 @@
import { getApiUrl, doExtrasFetch } from "../../extensions.js";
export { WhisperSttProvider }
const DEBUG_PREFIX = "<Speech Recognition module (Vosk)> "
class WhisperSttProvider {
//########//
// Config //
//########//
settings
defaultSettings = {
//model_path: "",
}
get settingsHtml() {
let html = ""
return html
}
onSettingsChange() {
// Used when provider settings are updated from UI
}
loadSettings(settings) {
// Populate Provider UI given input settings
if (Object.keys(settings).length == 0) {
console.debug(DEBUG_PREFIX+"Using default Whisper STT extension settings")
}
// Only accept keys defined in defaultSettings
this.settings = this.defaultSettings
for (const key in settings){
if (key in this.settings){
this.settings[key] = settings[key]
} else {
throw `Invalid setting passed to STT extension: ${key}`
}
}
console.debug(DEBUG_PREFIX+"Whisper STT settings loaded")
}
async processAudio(audioblob) {
var requestData = new FormData();
requestData.append('AudioFile', audioblob, 'record.wav');
const url = new URL(getApiUrl());
url.pathname = '/api/speech-recognition/whisper/process-audio';
const apiResult = await doExtrasFetch(url, {
method: 'POST',
body: requestData,
});
if (!apiResult.ok) {
toastr.error(apiResult.statusText, 'STT Generation Failed (Whisper)', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
throw new Error(`HTTP ${apiResult.status}: ${await apiResult.text()}`);
}
const result = await apiResult.json();
return result.transcript;
}
}

View File

@@ -36,6 +36,7 @@ const sources = {
auto: 'auto',
novel: 'novel',
vlad: 'vlad',
openai: 'openai',
}
const generationMode = {
@@ -69,6 +70,18 @@ const triggerWords = {
[generationMode.BACKGROUND]: ['background'],
}
const messageTrigger = {
activationRegex: /\b(send|mail|imagine|generate|make|create|draw|paint|render)\b.*\b(pic|picture|image|drawing|painting|photo|photograph)\b(?:\s+of)?(?:\s+(?:a|an|the|this|that|those)?)?(.+)/i,
specialCases: {
[generationMode.CHARACTER]: ['you', 'yourself'],
[generationMode.USER]: ['me', 'myself'],
[generationMode.SCENARIO]: ['story', 'scenario', 'whole story'],
[generationMode.NOW]: ['last message'],
[generationMode.FACE]: ['your face', 'your portrait', 'your selfie'],
[generationMode.BACKGROUND]: ['background', 'scene background', 'scene', 'scenery', 'surroundings', 'environment'],
},
}
const promptTemplates = {
/*OLD: [generationMode.CHARACTER]: "Pause your roleplay and provide comma-delimited list of phrases and keywords which describe {{char}}'s physical appearance and clothing. Ignore {{char}}'s personality traits, and chat history when crafting this description. End your response once the comma-delimited list is complete. Do not roleplay when writing this description, and do not attempt to continue the story.", */
[generationMode.CHARACTER]: "[In the next response I want you to provide only a detailed comma-delimited list of keywords and phrases which describe {{char}}. The list must include all of the following items in this order: name, species and race, gender, age, clothing, occupation, physical features and appearances. Do not include descriptions of non-visual qualities such as personality, movements, scents, mental traits, or anything which could not be seen in a still photograph. Do not write in full sentences. Prefix your description with the phrase 'full body portrait,']",
@@ -107,19 +120,20 @@ const promptTemplates = {
}
const helpString = [
`${m('(argument)')} requests SD to make an image. Supported arguments:`,
'<ul>',
`<li>${m(j(triggerWords[generationMode.CHARACTER]))} AI character full body selfie</li>`,
`<li>${m(j(triggerWords[generationMode.FACE]))} AI character face-only selfie</li>`,
`<li>${m(j(triggerWords[generationMode.USER]))} user character full body selfie</li>`,
`<li>${m(j(triggerWords[generationMode.SCENARIO]))} visual recap of the whole chat scenario</li>`,
`<li>${m(j(triggerWords[generationMode.NOW]))} visual recap of the last chat message</li>`,
`<li>${m(j(triggerWords[generationMode.RAW_LAST]))} visual recap of the last chat message with no summary</li>`,
`<li>${m(j(triggerWords[generationMode.BACKGROUND]))} generate a background for this chat based on the chat's context</li>`,
'</ul>',
`Anything else would trigger a "free mode" to make SD generate whatever you prompted.<Br>
example: '/sd apple tree' would generate a picture of an apple tree.`,
].join('<br>');
`${m('(argument)')} requests to generate an image. Supported arguments: ${m(j(Object.values(triggerWords).flat()))}.`,
`Anything else would trigger a "free mode" to make generate whatever you prompted. Example: '/imagine apple tree' would generate a picture of an apple tree.`,
].join(' ');
const defaultPrefix = 'best quality, absurdres, aesthetic,';
const defaultNegative = 'lowres, bad anatomy, bad hands, text, error, cropped, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, username, blurry';
const defaultStyles = [
{
name: 'Default',
negative: defaultNegative,
prefix: defaultPrefix,
},
];
const defaultSettings = {
source: sources.extras,
@@ -143,8 +157,8 @@ const defaultSettings = {
width: 512,
height: 512,
prompt_prefix: 'best quality, absurdres, masterpiece,',
negative_prompt: 'lowres, bad anatomy, bad hands, text, error, cropped, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, username, blurry',
prompt_prefix: defaultPrefix,
negative_prompt: defaultNegative,
sampler: 'DDIM',
model: '',
@@ -156,9 +170,12 @@ const defaultSettings = {
horde: false,
horde_nsfw: false,
horde_karras: true,
horde_sanitize: true,
// Refine mode
refine_mode: false,
expand: false,
interactive_mode: false,
prompts: promptTemplates,
@@ -189,8 +206,71 @@ const defaultSettings = {
novel_upscale_ratio_step: 0.1,
novel_upscale_ratio: 1.0,
novel_anlas_guard: false,
// OpenAI settings
openai_style: 'vivid',
openai_quality: 'standard',
style: 'Default',
styles: defaultStyles,
}
function processTriggers(chat, _, abort) {
if (!extension_settings.sd.interactive_mode) {
return;
}
const lastMessage = chat[chat.length - 1];
if (!lastMessage) {
return;
}
const message = lastMessage.mes;
const isUser = lastMessage.is_user;
if (!message || !isUser) {
return;
}
const messageLower = message.toLowerCase();
try {
const activationRegex = new RegExp(messageTrigger.activationRegex, 'i');
const activationMatch = messageLower.match(activationRegex);
if (!activationMatch) {
return;
}
let subject = activationMatch[3].trim();
if (!subject) {
return;
}
console.log(`SD: Triggered by "${message}", detected subject: ${subject}"`);
outer: for (const [specialMode, triggers] of Object.entries(messageTrigger.specialCases)) {
for (const trigger of triggers) {
if (subject === trigger) {
subject = triggerWords[specialMode][0];
console.log(`SD: Detected special case "${trigger}", switching to mode ${specialMode}`);
break outer;
}
}
}
abort(true);
setTimeout(() => generatePicture('sd', subject, message), 1);
} catch {
console.log('SD: Failed to process triggers.');
return;
}
}
window['SD_ProcessTriggers'] = processTriggers;
function getSdRequestBody() {
switch (extension_settings.sd.source) {
case sources.vlad:
@@ -237,6 +317,10 @@ async function loadSettings() {
extension_settings.sd.character_prompts = {};
}
if (!Array.isArray(extension_settings.sd.styles)) {
extension_settings.sd.styles = defaultStyles;
}
$('#sd_source').val(extension_settings.sd.source);
$('#sd_scale').val(extension_settings.sd.scale).trigger('input');
$('#sd_steps').val(extension_settings.sd.steps).trigger('input');
@@ -252,13 +336,26 @@ async function loadSettings() {
$('#sd_horde').prop('checked', extension_settings.sd.horde);
$('#sd_horde_nsfw').prop('checked', extension_settings.sd.horde_nsfw);
$('#sd_horde_karras').prop('checked', extension_settings.sd.horde_karras);
$('#sd_horde_sanitize').prop('checked', extension_settings.sd.horde_sanitize);
$('#sd_restore_faces').prop('checked', extension_settings.sd.restore_faces);
$('#sd_enable_hr').prop('checked', extension_settings.sd.enable_hr);
$('#sd_refine_mode').prop('checked', extension_settings.sd.refine_mode);
$('#sd_expand').prop('checked', extension_settings.sd.expand);
$('#sd_auto_url').val(extension_settings.sd.auto_url);
$('#sd_auto_auth').val(extension_settings.sd.auto_auth);
$('#sd_vlad_url').val(extension_settings.sd.vlad_url);
$('#sd_vlad_auth').val(extension_settings.sd.vlad_auth);
$('#sd_interactive_mode').prop('checked', extension_settings.sd.interactive_mode);
$('#sd_openai_style').val(extension_settings.sd.openai_style);
$('#sd_openai_quality').val(extension_settings.sd.openai_quality);
for (const style of extension_settings.sd.styles) {
const option = document.createElement('option');
option.value = style.name;
option.text = style.name;
option.selected = style.name === extension_settings.sd.style;
$('#sd_style').append(option);
}
toggleSourceControls();
addPromptTemplates();
@@ -276,7 +373,7 @@ function addPromptTemplates() {
const textarea = $('<textarea></textarea>')
.addClass('textarea_compact text_pole')
.attr('id', `sd_prompt_${name}`)
.attr('rows', 6)
.attr('rows', 3)
.val(prompt).on('input', () => {
extension_settings.sd.prompts[name] = textarea.val();
saveSettingsDebounced();
@@ -298,7 +395,93 @@ function addPromptTemplates() {
}
}
async function refinePrompt(prompt) {
function onInteractiveModeInput() {
extension_settings.sd.interactive_mode = !!$(this).prop('checked');
saveSettingsDebounced();
}
function onStyleSelect() {
const selectedStyle = String($('#sd_style').find(':selected').val());
const styleObject = extension_settings.sd.styles.find(x => x.name === selectedStyle);
if (!styleObject) {
console.warn(`Could not find style object for ${selectedStyle}`);
return;
}
$('#sd_prompt_prefix').val(styleObject.prefix).trigger('input');
$('#sd_negative_prompt').val(styleObject.negative).trigger('input');
extension_settings.sd.style = selectedStyle;
saveSettingsDebounced();
}
async function onSaveStyleClick() {
const userInput = await callPopup('Enter style name:', 'input', '', { okButton: 'Save' });
if (!userInput) {
return;
}
const name = String(userInput).trim();
const prefix = String($('#sd_prompt_prefix').val());
const negative = String($('#sd_negative_prompt').val());
const existingStyle = extension_settings.sd.styles.find(x => x.name === name);
if (existingStyle) {
existingStyle.prefix = prefix;
existingStyle.negative = negative;
$('#sd_style').val(name);
saveSettingsDebounced();
return;
}
const styleObject = {
name: name,
prefix: prefix,
negative: negative,
};
extension_settings.sd.styles.push(styleObject);
const option = document.createElement('option');
option.value = styleObject.name;
option.text = styleObject.name;
option.selected = true;
$('#sd_style').append(option);
$('#sd_style').val(styleObject.name);
saveSettingsDebounced();
}
async function expandPrompt(prompt) {
try {
const response = await fetch('/api/sd/expand', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ prompt: prompt }),
});
if (!response.ok) {
throw new Error('API returned an error.');
}
const data = await response.json();
return data.prompt;
} catch {
return prompt;
}
}
/**
* Modifies prompt based on auto-expansion and user inputs.
* @param {string} prompt Prompt to refine
* @param {boolean} allowExpand Whether to allow auto-expansion
* @returns {Promise<string>} Refined prompt
*/
async function refinePrompt(prompt, allowExpand) {
if (allowExpand && extension_settings.sd.expand) {
prompt = await expandPrompt(prompt);
}
if (extension_settings.sd.refine_mode) {
const refinedPrompt = await callPopup('<h3>Review and edit the prompt:</h3>Press "Cancel" to abort the image generation.', 'input', prompt.trim(), { rows: 5, okButton: 'Generate' });
@@ -344,7 +527,14 @@ function getCharacterPrefix() {
return '';
}
function combinePrefixes(str1, str2) {
/**
* Combines two prompt prefixes into one.
* @param {string} str1 Base string
* @param {string} str2 Secondary string
* @param {string} macro Macro to replace with the secondary string
* @returns {string} Combined string with a comma between them
*/
function combinePrefixes(str1, str2, macro = '') {
if (!str2) {
return str1;
}
@@ -353,12 +543,16 @@ function combinePrefixes(str1, str2) {
str1 = str1.trim().replace(/^,|,$/g, '');
str2 = str2.trim().replace(/^,|,$/g, '');
// Combine the strings with a comma between them
var result = `${str1}, ${str2},`;
// Combine the strings with a comma between them)
const result = macro && str1.includes(macro) ? str1.replace(macro, str2) : `${str1}, ${str2},`;
return result;
}
function onExpandInput() {
extension_settings.sd.expand = !!$(this).prop('checked');
saveSettingsDebounced();
}
function onRefineModeInput() {
extension_settings.sd.refine_mode = !!$('#sd_refine_mode').prop('checked');
saveSettingsDebounced();
@@ -414,6 +608,16 @@ async function onSourceChange() {
await Promise.all([loadModels(), loadSamplers()]);
}
async function onOpenAiStyleSelect() {
extension_settings.sd.openai_style = String($('#sd_openai_style').find(':selected').val());
saveSettingsDebounced();
}
async function onOpenAiQualitySelect() {
extension_settings.sd.openai_quality = String($('#sd_openai_quality').find(':selected').val());
saveSettingsDebounced();
}
async function onViewAnlasClick() {
const result = await loadNovelSubscriptionData();
@@ -439,16 +643,21 @@ function onNovelAnlasGuardInput() {
saveSettingsDebounced();
}
async function onHordeNsfwInput() {
function onHordeNsfwInput() {
extension_settings.sd.horde_nsfw = !!$(this).prop('checked');
saveSettingsDebounced();
}
async function onHordeKarrasInput() {
function onHordeKarrasInput() {
extension_settings.sd.horde_karras = !!$(this).prop('checked');
saveSettingsDebounced();
}
function onHordeSanitizeInput() {
extension_settings.sd.horde_sanitize = !!$(this).prop('checked');
saveSettingsDebounced();
}
function onRestoreFacesInput() {
extension_settings.sd.restore_faces = !!$(this).prop('checked');
saveSettingsDebounced();
@@ -554,7 +763,7 @@ async function onModelChange() {
extension_settings.sd.model = $('#sd_model').find(':selected').val();
saveSettingsDebounced();
const cloudSources = [sources.horde, sources.novel];
const cloudSources = [sources.horde, sources.novel, sources.openai];
if (cloudSources.includes(extension_settings.sd.source)) {
return;
@@ -567,7 +776,7 @@ async function onModelChange() {
if (extension_settings.sd.source === sources.auto || extension_settings.sd.source === sources.vlad) {
await updateAutoRemoteModel();
}
toastr.success('Model successfully loaded!', 'Stable Diffusion');
toastr.success('Model successfully loaded!', 'Image Generation');
}
async function getAutoRemoteModel() {
@@ -682,6 +891,9 @@ async function loadSamplers() {
case sources.vlad:
samplers = await loadVladSamplers();
break;
case sources.openai:
samplers = await loadOpenAiSamplers();
break;
}
for (const sampler of samplers) {
@@ -747,6 +959,10 @@ async function loadAutoSamplers() {
}
}
async function loadOpenAiSamplers() {
return ['N/A'];
}
async function loadVladSamplers() {
if (!extension_settings.sd.vlad_url) {
return [];
@@ -807,6 +1023,9 @@ async function loadModels() {
case sources.vlad:
models = await loadVladModels();
break;
case sources.openai:
models = await loadOpenAiModels();
break;
}
for (const model of models) {
@@ -904,6 +1123,13 @@ async function loadAutoModels() {
}
}
async function loadOpenAiModels() {
return [
{ value: 'dall-e-2', text: 'DALL-E 2' },
{ value: 'dall-e-3', text: 'DALL-E 3' },
];
}
async function loadVladModels() {
if (!extension_settings.sd.vlad_url) {
return [];
@@ -954,17 +1180,21 @@ async function loadNovelModels() {
}
return [
{
value: 'nai-diffusion-2',
text: 'NAI Diffusion Anime V2',
},
{
value: 'nai-diffusion',
text: 'Full',
text: 'NAI Diffusion Anime V1 (Full)',
},
{
value: 'safe-diffusion',
text: 'Safe',
text: 'NAI Diffusion Anime V1 (Curated)',
},
{
value: 'nai-diffusion-furry',
text: 'Furry',
text: 'NAI Diffusion Furry',
},
];
}
@@ -1021,7 +1251,7 @@ function getRawLastMessage() {
return message.mes;
}
toastr.warning('No usable messages found.', 'Stable Diffusion');
toastr.warning('No usable messages found.', 'Image Generation');
throw new Error('No usable messages found.');
}
@@ -1056,6 +1286,42 @@ async function generatePicture(_, trigger, message, callback) {
// sadly, groups is not an array, but is a dict with keys being index numbers, so we have to filter it
const characterName = context.characterId ? context.characters[context.characterId].name : context.groups[Object.keys(context.groups).filter(x => context.groups[x].id === context.groupId)[0]]?.id?.toString();
if (generationType == generationMode.BACKGROUND) {
const callbackOriginal = callback;
callback = async function (prompt, imagePath, generationType) {
const imgUrl = `url("${encodeURI(imagePath)}")`;
eventSource.emit(event_types.FORCE_SET_BACKGROUND, { url: imgUrl, path: imagePath });
if (typeof callbackOriginal === 'function') {
callbackOriginal(prompt, imagePath, generationType);
} else {
sendMessage(prompt, imagePath, generationType);
}
}
}
const dimensions = setTypeSpecificDimensions(generationType);
try {
const prompt = await getPrompt(generationType, message, trigger, quiet_prompt);
console.log('Processed image prompt:', prompt);
context.deactivateSendButtons();
hideSwipeButtons();
await sendGenerationRequest(generationType, prompt, characterName, callback);
} catch (err) {
console.trace(err);
throw new Error('SD prompt text generation failed.')
}
finally {
restoreOriginalDimensions(dimensions);
context.activateSendButtons();
showSwipeButtons();
}
}
function setTypeSpecificDimensions(generationType) {
const prevSDHeight = extension_settings.sd.height;
const prevSDWidth = extension_settings.sd.width;
const aspectRatio = extension_settings.sd.width / extension_settings.sd.height;
@@ -1072,38 +1338,14 @@ async function generatePicture(_, trigger, message, callback) {
// Round to nearest multiple of 64
extension_settings.sd.width = Math.round(extension_settings.sd.height * 1.8 / 64) * 64;
}
const callbackOriginal = callback;
callback = async function (prompt, base64Image) {
const imagePath = base64Image;
const imgUrl = `url("${encodeURI(base64Image)}")`;
eventSource.emit(event_types.FORCE_SET_BACKGROUND, imgUrl);
if (typeof callbackOriginal === 'function') {
callbackOriginal(prompt, imagePath);
} else {
sendMessage(prompt, imagePath);
}
}
}
try {
const prompt = await getPrompt(generationType, message, trigger, quiet_prompt);
console.log('Processed Stable Diffusion prompt:', prompt);
return { height: prevSDHeight, width: prevSDWidth };
}
context.deactivateSendButtons();
hideSwipeButtons();
await sendGenerationRequest(generationType, prompt, characterName, callback);
} catch (err) {
console.trace(err);
throw new Error('SD prompt text generation failed.')
}
finally {
extension_settings.sd.height = prevSDHeight;
extension_settings.sd.width = prevSDWidth;
context.activateSendButtons();
showSwipeButtons();
}
function restoreOriginalDimensions(savedParams) {
extension_settings.sd.height = savedParams.height;
extension_settings.sd.width = savedParams.width;
}
async function getPrompt(generationType, message, trigger, quiet_prompt) {
@@ -1122,23 +1364,23 @@ async function getPrompt(generationType, message, trigger, quiet_prompt) {
}
if (generationType !== generationMode.FREE) {
prompt = await refinePrompt(prompt);
prompt = await refinePrompt(prompt, true);
}
return prompt;
}
async function generatePrompt(quiet_prompt) {
const reply = await generateQuietPrompt(quiet_prompt, false);
const reply = await generateQuietPrompt(quiet_prompt, false, false);
return processReply(reply);
}
async function sendGenerationRequest(generationType, prompt, characterName = null, callback) {
const prefix = generationType !== generationMode.BACKGROUND
const prefix = (generationType !== generationMode.BACKGROUND && generationType !== generationMode.FREE)
? combinePrefixes(extension_settings.sd.prompt_prefix, getCharacterPrefix())
: extension_settings.sd.prompt_prefix;
const prefixedPrompt = combinePrefixes(prefix, prompt);
const prefixedPrompt = combinePrefixes(prefix, prompt, '{prompt}');
let result = { format: '', data: '' };
const currentChatId = getCurrentChatId();
@@ -1160,25 +1402,29 @@ async function sendGenerationRequest(generationType, prompt, characterName = nul
case sources.novel:
result = await generateNovelImage(prefixedPrompt);
break;
case sources.openai:
result = await generateOpenAiImage(prefixedPrompt);
break;
}
if (!result.data) {
throw new Error();
throw new Error('Endpoint did not return image data.');
}
} catch (err) {
toastr.error('Image generation failed. Please try again', 'Stable Diffusion');
console.error(err);
toastr.error('Image generation failed. Please try again.' + '\n\n' + String(err), 'Image Generation');
return;
}
if (currentChatId !== getCurrentChatId()) {
console.warn('Chat changed, aborting SD result saving');
toastr.warning('Chat changed, generated image discarded.', 'Stable Diffusion');
toastr.warning('Chat changed, generated image discarded.', 'Image Generation');
return;
}
const filename = `${characterName}_${humanizedDateTime()}`;
const base64Image = await saveBase64AsFile(result.data, characterName, filename, result.format);
callback ? callback(prompt, base64Image) : sendMessage(prompt, base64Image);
callback ? callback(prompt, base64Image, generationType) : sendMessage(prompt, base64Image, generationType);
}
/**
@@ -1217,7 +1463,8 @@ async function generateExtrasImage(prompt) {
const data = await result.json();
return { format: 'jpg', data: data.image };
} else {
throw new Error();
const text = await result.text();
throw new Error(text);
}
}
@@ -1243,6 +1490,7 @@ async function generateHordeImage(prompt) {
nsfw: extension_settings.sd.horde_nsfw,
restore_faces: !!extension_settings.sd.restore_faces,
enable_hr: !!extension_settings.sd.enable_hr,
sanitize: !!extension_settings.sd.horde_sanitize,
}),
});
@@ -1250,7 +1498,8 @@ async function generateHordeImage(prompt) {
const data = await result.text();
return { format: 'webp', data: data };
} else {
throw new Error();
const text = await result.text();
throw new Error(text);
}
}
@@ -1291,7 +1540,8 @@ async function generateAutoImage(prompt) {
const data = await result.json();
return { format: 'png', data: data.images[0] };
} else {
throw new Error();
const text = await result.text();
throw new Error(text);
}
}
@@ -1324,7 +1574,8 @@ async function generateNovelImage(prompt) {
const data = await result.text();
return { format: 'png', data: data };
} else {
throw new Error();
const text = await result.text();
throw new Error(text);
}
}
@@ -1337,13 +1588,13 @@ function getNovelParams() {
let width = extension_settings.sd.width;
let height = extension_settings.sd.height;
// Don't apply Anlas guard if it's disabled.d
// Don't apply Anlas guard if it's disabled.
if (!extension_settings.sd.novel_anlas_guard) {
return { steps, width, height };
}
const MAX_STEPS = 28;
const MAX_PIXELS = 409600;
const MAX_PIXELS = 1024 * 1024;
if (width * height > MAX_PIXELS) {
const ratio = Math.sqrt(MAX_PIXELS / (width * height));
@@ -1383,7 +1634,62 @@ function getNovelParams() {
return { steps, width, height };
}
async function sendMessage(prompt, image) {
async function generateOpenAiImage(prompt) {
const dalle2PromptLimit = 1000;
const dalle3PromptLimit = 4000;
const isDalle2 = extension_settings.sd.model === 'dall-e-2';
const isDalle3 = extension_settings.sd.model === 'dall-e-3';
if (isDalle2 && prompt.length > dalle2PromptLimit) {
prompt = prompt.substring(0, dalle2PromptLimit);
}
if (isDalle3 && prompt.length > dalle3PromptLimit) {
prompt = prompt.substring(0, dalle3PromptLimit);
}
let width = 1024;
let height = 1024;
let aspectRatio = extension_settings.sd.width / extension_settings.sd.height;
if (isDalle3 && aspectRatio < 1) {
height = 1792;
}
if (isDalle3 && aspectRatio > 1) {
width = 1792;
}
if (isDalle2 && (extension_settings.sd.width <= 512 && extension_settings.sd.height <= 512)) {
width = 512;
height = 512;
}
const result = await fetch('/api/openai/generate-image', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({
prompt: prompt,
model: extension_settings.sd.model,
size: `${width}x${height}`,
n: 1,
quality: isDalle3 ? extension_settings.sd.openai_quality : undefined,
style: isDalle3 ? extension_settings.sd.openai_style : undefined,
response_format: 'b64_json',
}),
});
if (result.ok) {
const data = await result.json();
return { format: 'png', data: data?.data[0]?.b64_json };
} else {
const text = await result.text();
throw new Error(text);
}
}
async function sendMessage(prompt, image, generationType) {
const context = getContext();
const messageText = `[${context.name2} sends a picture that contains: ${prompt}]`;
const message = {
@@ -1395,6 +1701,7 @@ async function sendMessage(prompt, image) {
extra: {
image: image,
title: prompt,
generationType: generationType,
},
};
context.chat.push(message);
@@ -1407,7 +1714,7 @@ function addSDGenButtons() {
const buttonHtml = `
<div id="sd_gen" class="list-group-item flex-container flexGap5">
<div class="fa-solid fa-paintbrush extensionsMenuExtensionButton" title="Trigger Stable Diffusion" /></div>
Stable Diffusion
Generate Image
</div>
`;
@@ -1473,6 +1780,8 @@ function isValidState() {
return !!extension_settings.sd.vlad_url;
case sources.novel:
return secret_state[SECRET_KEYS.NOVEL];
case sources.openai:
return secret_state[SECRET_KEYS.OPENAI];
}
}
@@ -1512,14 +1821,18 @@ async function sdMessageButton(e) {
return;
}
let dimensions = null;
try {
setBusyIcon(true);
if (hasSavedImage) {
const prompt = await refinePrompt(message.extra.title);
const prompt = await refinePrompt(message.extra.title, false);
message.extra.title = prompt;
const generationType = message?.extra?.generationType ?? generationMode.FREE;
console.log('Regenerating an image, using existing prompt:', prompt);
await sendGenerationRequest(generationMode.FREE, prompt, characterFileName, saveGeneratedImage);
dimensions = setTypeSpecificDimensions(generationType);
await sendGenerationRequest(generationType, prompt, characterFileName, saveGeneratedImage);
}
else {
console.log("doing /sd raw last");
@@ -1531,9 +1844,13 @@ async function sdMessageButton(e) {
}
finally {
setBusyIcon(false);
if (dimensions) {
restoreOriginalDimensions(dimensions);
}
}
function saveGeneratedImage(prompt, image) {
function saveGeneratedImage(prompt, image, generationType) {
// Some message sources may not create the extra object
if (typeof message.extra !== 'object') {
message.extra = {};
@@ -1543,6 +1860,7 @@ async function sdMessageButton(e) {
message.extra.inline_image = message.extra.image && !message.extra.inline_image ? false : true;
message.extra.image = image;
message.extra.title = prompt;
message.extra.generationType = generationType;
appendImageToMessage(message, $mes);
context.saveChat();
@@ -1570,7 +1888,7 @@ $("#sd_dropdown [id]").on("click", function () {
});
jQuery(async () => {
getContext().registerSlashCommand('sd', generatePicture, [], helpString, true, true);
getContext().registerSlashCommand('imagine', generatePicture, ['sd', 'img', 'image'], helpString, true, true);
$('#extensions_settings').append(renderExtensionTemplate('stable-diffusion', 'settings', defaultSettings));
$('#sd_source').on('change', onSourceChange);
@@ -1584,6 +1902,7 @@ jQuery(async () => {
$('#sd_height').on('input', onHeightInput);
$('#sd_horde_nsfw').on('input', onHordeNsfwInput);
$('#sd_horde_karras').on('input', onHordeKarrasInput);
$('#sd_horde_sanitize').on('input', onHordeSanitizeInput);
$('#sd_restore_faces').on('input', onRestoreFacesInput);
$('#sd_enable_hr').on('input', onHighResFixInput);
$('#sd_refine_mode').on('input', onRefineModeInput);
@@ -1601,7 +1920,13 @@ jQuery(async () => {
$('#sd_novel_upscale_ratio').on('input', onNovelUpscaleRatioInput);
$('#sd_novel_anlas_guard').on('input', onNovelAnlasGuardInput);
$('#sd_novel_view_anlas').on('click', onViewAnlasClick);
$('#sd_expand').on('input', onExpandInput);
$('#sd_style').on('change', onStyleSelect);
$('#sd_save_style').on('click', onSaveStyleClick);
$('#sd_character_prompt_block').hide();
$('#sd_interactive_mode').on('input', onInteractiveModeInput);
$('#sd_openai_style').on('change', onOpenAiStyleSelect);
$('#sd_openai_quality').on('change', onOpenAiQualitySelect);
$('.sd_settings .inline-drawer-toggle').on('click', function () {
initScrollHeight($("#sd_prompt_prefix"));

Some files were not shown because too many files have changed in this diff Show More