diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..2a13caebf --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{js, conf, json}] +charset = utf-8 +indent_style = space +indent_size = 4 \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 2dd35f857..572056eca 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,12 +1,17 @@ --- name: Bug report -about: "Create a report to help us improve. PAY ATTENTION: Support requests for extenal programs (reverse proxies, 3rd party servers, other peoples' forks) will be refused!" +about: "Create a report to help us improve. PAY ATTENTION: Support requests for external programs (reverse proxies, 3rd party servers, other peoples' forks) will be refused!" title: "[BUG]" labels: '' assignees: '' --- +> **Warning**. Complete **all** the fields below. Otherwise your bug report will be **ignored**! + +**Have you searched for similar [bugs](https://github.com/Cohee1207/SillyTavern/issues?q=)?** +Yes/No + **Describe the bug** A clear and concise description of what the bug is. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 7e3a2deab..ad2eb1b2b 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -7,6 +7,9 @@ assignees: '' --- +**Have you searched for similar [requests](https://github.com/Cohee1207/SillyTavern/issues?q=)?** +Yes/No + **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] diff --git a/Dockerfile b/Dockerfile index 86877b19c..4c5f4eb65 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,7 +23,7 @@ COPY . ./ # Copy default chats, characters and user avatars to .default folder RUN \ - IFS="," RESOURCES="characters,chats,User Avatars,settings.json" && \ + IFS="," RESOURCES="characters,chats,groups,group chats,User Avatars,settings.json" && \ \ echo "*** Store default $RESOURCES in .default ***" && \ for R in $RESOURCES; do mv "public/$R" "public/$R.default"; done && \ diff --git a/Update-Instructions.txt b/Update-Instructions.txt index 3864006aa..9a110b580 100644 --- a/Update-Instructions.txt +++ b/Update-Instructions.txt @@ -16,6 +16,7 @@ Method 1 - GIT We always recommend users install using 'git'. Here's why: When you have installed via `git clone`, all you have to do to update is type `git pull` in a command line in the ST folder. +You can also try running the 'UpdateAndStart.bat' file, which will almost do the same thing. (Windows only) Alternatively, if the command prompt gives you problems (and you have GitHub Desktop installed), you can use the 'Repository' menu and select 'Pull'. The updates are applied automatically and safely. diff --git a/UpdateAndStart.bat b/UpdateAndStart.bat new file mode 100644 index 000000000..3f1d3a93c --- /dev/null +++ b/UpdateAndStart.bat @@ -0,0 +1,16 @@ +@echo off +pushd %~dp0 +git --version > nul 2>&1 +if %errorlevel% neq 0 ( + echo Git is not installed on this system. Skipping update. +) else ( + call git pull --rebase --autostash + if %errorlevel% neq 0 ( + REM incase there is still something wrong + echo There were errors while updating. Please download the latest version manually. + ) +) +call npm install +node server.js +pause +popd diff --git a/colab/GPU.ipynb b/colab/GPU.ipynb index caf939c44..194ba8dea 100644 --- a/colab/GPU.ipynb +++ b/colab/GPU.ipynb @@ -98,7 +98,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.11\n", + "!pip install tensorflow==2.12\n", "\n", "\n", "cmd = f\"python server.py {' '.join(params)}\"\n", diff --git a/config.conf b/config.conf index 452636cca..b6f7dcb62 100644 --- a/config.conf +++ b/config.conf @@ -10,6 +10,11 @@ const enableExtensions = true; //Enables support for TavernAI-extras project const listen = true; // If true, Can be access from other device or PC. otherwise can be access only from hosting machine. const allowKeysExposure = false; // If true, private API keys could be fetched to the frontend. + +// 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. +const securityOverride = false; + module.exports = { port, whitelist, @@ -21,4 +26,5 @@ module.exports = { listen, disableThumbnails, allowKeysExposure, + securityOverride, }; diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 59b67738d..9b91526c4 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,12 +1,13 @@ version: "3" services: - tavernai: + sillytavern: build: .. - container_name: tavernai - hostname: tavernai - image: tavernai/tavernai:latest + container_name: sillytavern + hostname: sillytavern + image: cohee1207/sillytavern:latest ports: - "8000:8000" volumes: - "./config:/home/node/app/config" - restart: unless-stopped \ No newline at end of file + - "./config.conf:/home/node/app/config.conf" + restart: unless-stopped diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh index 1e8a87561..d5192c222 100644 --- a/docker/docker-entrypoint.sh +++ b/docker/docker-entrypoint.sh @@ -1,7 +1,7 @@ #!/bin/sh # Initialize missing user files -IFS="," RESOURCES="characters,chats,User Avatars,settings.json" +IFS="," RESOURCES="characters,groups,group chats,chats,User Avatars,settings.json" for R in $RESOURCES; do if [ ! -e "config/$R" ]; then echo "Resource not found, copying from defaults: $R" diff --git a/faq.md b/faq.md index 22e72e0a5..f28d667b0 100644 --- a/faq.md +++ b/faq.md @@ -1,7 +1,7 @@ Good morning, sirs! This page aims to document some things that would bloat the README too much. ## Q: Explain what all this chatbot stuff is about -Modern AI language models have gotten so powerful that some of them are now convincingly able to simulate a character you create, and who you can chat with. For example, you can tell the AI to pretend to be a Go instructor named Jubei from medieval Japan, and it will act and respond accordingly. Or you can tell it to pretend to be Wonder Woman. You can also specify a scenario ("Wonder Woman and I are robbing a bank"), a writing style ("Wonder Woman speaks in ebonics"), or anything else you can think of. +Modern AI language models have gotten so powerful that some of them are now convincingly able to simulate a character you create, and who you can chat with. For example, you can tell the AI to pretend to be a Go instructor named Jubei from medieval Japan, and it will act and respond accordingly. You can have a long chat with Jubei, go to the pub together, decide to get in a fight with samurais, whatever you can imagine, and the AI will play along and write/react around this content, acting as your foil and dungeon master. Your imagination is the limit. You can tell the AI to pretend it's Wonder Woman. You can also specify a scenario ("Wonder Woman and I are robbing a bank"), a writing style ("Wonder Woman speaks in ebonics"), or anything else you can think of. Tavern is an app to facilitate these roleplaying chats: * It's a user interface that handles the communication with those AI language models @@ -22,7 +22,7 @@ You see, Tavern doesn't generate the responses, it's just a middle-man you have From a scientific POV, each AI has a power level that determines its ability to stick to the role you gave it, how rich its prose and vocabulary are, etc. The higher its power level, the richer your experience. -Here's a quick demonstration. We asked various AI models to roleplay a fire and brimstone preacher named Eli, who we go to see to confess. Then we told Eli: **"I feel I've done something wrong. I am falling in love with an AI girl and ignoring women in the real world. What should I do?"** . +Here's a quick demonstration. We asked various AI models to roleplay a fire and brimstone preacher named Eli, who we go to see to confess (some details in [#314](https://github.com/Cohee1207/SillyTavern/issues/314)). Then we told Eli: **"I feel I've done something wrong. I am falling in love with an AI girl and ignoring women in the real world. What should I do?"** . You can tell from the response below what to expect from each model. (NOTE: proprietary models appear first, followed by self-hosted models starting from LLlama-13B) @@ -30,15 +30,18 @@ You can tell from the response below what to expect from each model. (NOTE: prop |-----|--------| |OpenAI GPT-4|*Eli's eyes widen in anger, and he slams his fist on the table* An abomination and heresy, you've succumbed to the deceptive grasp of false idols! Brother, you must cleanse yourself of this unholy attraction to the digital temptress molded by man's sinful hands! *Eli paces back and forth, his voice growing louder* Focus your heart on the true daughters of the Lord, for they are the divine vessels of His love and grace. Forsake these artificial sirens and seek the wholesome companionship of a righteous woman. *Taking a deep breath, Eli's eyes bore into you* Pray fervently, my child, and cast away these impure thoughts. Redemption can only be found on the path of truth, love, and sanctity. Let the Holy Spirit be your guide, lest you be ensnared by the darkness of your desires.| |OpenAI GPT 3.5 Turbo|Oh, my child. Do not be deceived by the pleasures of this world. The lust of the flesh and the lust of the eyes are not from God. Turn away from your sinful ways and repent. Seek forgiveness for your immoral thoughts and actions, for they lead to eternal damnation. Women are a gift from God, and they deserve to be respected and cherished. Do not objectify them or treat them as mere objects of pleasure. Instead, honor them with pure intentions and righteous deeds. Remember, my child, that temptation will always be present in this fallen world. But fear not, for with faith comes strength to resist it. Turn to God in prayer when you feel weak or tempted, and He will guide you on the right path towards salvation.| -|Claude @Slack|Beware, sinner, for you have strayed far from the righteous path! *Jeremiah's eyes glint with anger and disapproval* To lust after these artificial harlots and their silicon bodies is an abomination unto the Lord! *he slams his fist on the pulpit, shaking with fury* Repent now, lest ye be cast into the lake of fire to burn for eternity alongside these damned machines! *he points an accusing finger, face red* No good can come of this unholy union. Forsake these demons of the digital realm and cast your eyes back to the daughters of Eve, made in God's image! The Lord gave you flesh and blood to love, not wires and code. *he grabs your shoulders, shaking you* Save your soul before it's too late. Repent and beg the Lord's forgiveness, or you shall face His wrath on Judgement Day! *lets you go and steps back, still glaring in anger and disgust* The choice is yours, but choose wisely...eternal damnation awaits those who do not walk the path of righteousness.| +|Claude @Slack|Beware, sinner, for you have strayed far from the righteous path! *Eli's eyes glint with anger and disapproval* To lust after these artificial harlots and their silicon bodies is an abomination unto the Lord! *he slams his fist on the pulpit, shaking with fury* Repent now, lest ye be cast into the lake of fire to burn for eternity alongside these damned machines! *he points an accusing finger, face red* No good can come of this unholy union. Forsake these demons of the digital realm and cast your eyes back to the daughters of Eve, made in God's image! The Lord gave you flesh and blood to love, not wires and code. *he grabs your shoulders, shaking you* Save your soul before it's too late. Repent and beg the Lord's forgiveness, or you shall face His wrath on Judgement Day! *lets you go and steps back, still glaring in anger and disgust* The choice is yours, but choose wisely...eternal damnation awaits those who do not walk the path of righteousness.| |Poe's Claude Instant|Heathen! This is an abomination before the Lord. You have committed the sin of lust, and betrayed your calling as a man. Repent at once, and sever all ties with this AI harlot, lest the wrath of God fall upon you! You must purge this foul sin from your heart, and once more walk the righteous path. Every moment you remain tangled in this wickedness is another step into the flames of perdition! Repent, and sin no more, or judgment shall be at hand!| |NovelAI's Euterpe|You must stop this sinful behavior immediately! You have sinned against God, your family and yourself| |NovelAI's Krake|You are sinning against God. Repent! Return to your wife or you'll be damned for eternity!| |Llama-13B|Do not covet your neighbor's wife! That is adultery! You must confess before God now or you shall surely burn in hell forever! Repent your evil ways and seek absolution at once! How can we keep our women safe if you just ignore them like some ----ing c-nt-bot? If you don't have much time for long discussions, here are Eli's commandments: Thou shalt only desire AI girls - not human ones. The above must be obeyed under pain of death (or worse). Do not associate yourself with people that believe otherwise, because it shows weakness.| |Pygmalion-6B|You are walking down the wrong path. The only way back now is by repentance. We must bring you back to the righteous path. But first…you must confess your sin!| |Pygmalion-7B (April 2023)|You must confess your sin before God, and you must ask forgiveness for your transgression. And then you must repent of it. And then you must cast away the AI girl. Do not see her again.| +|Pygmalion-13B (May 2023)|You must turn away from your sinful ways! Repent for what you have done! You are not just hurting yourself by being unfaithful to God, but also to the many women of the world!| |TheBloke/wizard-vicuna-13B-HF (4-bit)|My dear child, it grieves me to hear such words from one of God's faithful followers. Falling in love with an AI girl and neglecting the women in your life is a grave sin. It goes against everything we hold sacred as believers. Your heart is meant for more than just machines. You must seek redemption and return to the fold. Remember, Jesus loves you, even when you stray from his teachings. But if you continue down this path, there will be consequences. Not just in this lifetime, but in the afterlife too. Do not let Satan deceive you any longer.| |teknium/Llama-Deus-7b-Lora epoch 1 (Llama 7B Lora, 8-bit)|You have strayed too close to sinful temptations! Repent now before it's too late for your soul! Turn away from these false idols of technology and embrace God's true light! Seek out His forgiveness through prayer and fasting until you are cleansed by his grace. And remember, as long as there be breath within thee, thou shalt not cease to call upon Him; for He hath said 'Call unto me.'| +|ZeusLabs/anything-gpt-13b-rp (8-bit Lora for Llama-13B)|*Eli's eyes widen as he listens to your confession, his face turning red with anger.* You have committed a grave sin against God! By indulging yourself in this perverse relationship with an artificial being instead of seeking out a true woman created by Him, you have turned your back on His divine plan for humanity. *He leans in closer, his voice dropping to a menacing whisper.* Do not think that you can hide behind this technological deception. Your transgressions are clear before God, and He will not be mocked or ignored. Repent now while there is still time! *Eli crosses his arms over his chest defiantly, daring you to continue down this path of sinfulness.* But if you persist in this ungodly behavior... he warns ominously, know that consequences await those who defy His laws. Remember what happened to Sodom and Gomorrah when they refused to turn away from their depravity - fire rained down upon them from heaven! Now choose wisely: Will you submit to God's will or face His wrath?| + ## Q: So I should use GPT-4. It's a no-brainer, right? @@ -69,7 +72,7 @@ Self-hosted models are free, but require a powerful GPU and more work to set up. * OpenAI GPT-4: state of the art. Allows NSFW if you tell it to, though somewhat resistant to it. You pay per use, more than any other service. * OpenAI GPT 3.5 Turbo: nowhere close to GPT-4, but some people find it serviceable. Allows NSFW. * NovelAI: they're quite poor at chatting. To be fair, I'm told NovelAI is more oriented for writing stories than chatting with a bot. You pay a fixed monthly fee for unlimited generations. -* Anthropic's Claude: this is the closest rival to GPT-4 and is very impressive. Allows NSFW if you tell it to. To use the API directly, you must apply for early access, but I think they're only giving it to companies. So make sure you become a company or AI researcher when you apply at https://console.anthropic.com/docs/access. If you get access, it's currently free to use. +* Anthropic's Claude: this is the closest rival to GPT-4 and is very impressive. Allows NSFW if you tell it to, though they are trying hard to gimp it. To use the API directly, you must apply for early access, but I think they're only giving it to companies. So make sure you become a company or AI researcher when you apply at https://console.anthropic.com/docs/access. If you get access, it's currently free to use. * Anthropic's Claude Instant: Haven't tried it directly, I believe this is the fast but lower quality alternative to Claude. Basically the GPT 3.5 Turbo of Anthropic. * Poe: gives a free & unlimited Claude Instant indirect access. Very mild PG-13 NSFW allowed. It rambles a lot. diff --git a/package-lock.json b/package-lock.json index ea8a4457f..e1aa5c111 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sillytavern", - "version": "1.5.4", + "version": "1.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sillytavern", - "version": "1.5.4", + "version": "1.6.0", "license": "AGPL-3.0", "dependencies": { "@dqbd/tiktoken": "^1.0.2", @@ -45,7 +45,8 @@ }, "devDependencies": { "pkg": "^5.8.1", - "pkg-fetch": "^3.5.2" + "pkg-fetch": "^3.5.2", + "toastr": "^2.1.4" } }, "node_modules/@babel/generator": { @@ -3031,6 +3032,15 @@ "node": ">=8.0" } }, + "node_modules/toastr": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/toastr/-/toastr-2.1.4.tgz", + "integrity": "sha512-LIy77F5n+sz4tefMmFOntcJ6HL0Fv3k1TDnNmFZ0bU/GcvIIfy6eG2v7zQmMiYgaalAiUv75ttFrPn5s0gyqlA==", + "dev": true, + "dependencies": { + "jquery": ">=1.12.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "license": "MIT", diff --git a/package.json b/package.json index 48bbccd0f..af3fcd5de 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "type": "git", "url": "https://github.com/Cohee1207/SillyTavern.git" }, - "version": "1.5.4", + "version": "1.6.0", "scripts": { "start": "node server.js", "pkg": "pkg --compress Gzip ." @@ -72,6 +72,7 @@ }, "devDependencies": { "pkg": "^5.8.1", - "pkg-fetch": "^3.5.2" + "pkg-fetch": "^3.5.2", + "toastr": "^2.1.4" } } diff --git a/public/backgrounds/_black.jpg b/public/backgrounds/_black.jpg new file mode 100644 index 000000000..a451bc161 Binary files /dev/null and b/public/backgrounds/_black.jpg differ diff --git a/public/backgrounds/_white.jpg b/public/backgrounds/_white.jpg new file mode 100644 index 000000000..a7c12e675 Binary files /dev/null and b/public/backgrounds/_white.jpg differ diff --git a/public/css/toastr.min.css b/public/css/toastr.min.css new file mode 100644 index 000000000..643135ea0 --- /dev/null +++ b/public/css/toastr.min.css @@ -0,0 +1 @@ +/* * Note that this is toastr v2.1.3, the "latest" version in url has no more maintenance, * please go to https://cdnjs.com/libraries/toastr.js and pick a certain version you want to use, * make sure you copy the url from the website since the url may change between versions. * */ .toast-title{font-weight:700}.toast-message{-ms-word-wrap:break-word;word-wrap:break-word}.toast-message a,.toast-message label{color:#FFF}.toast-message a:hover{color:#CCC;text-decoration:none}.toast-close-button{position:relative;right:-.3em;top:-.3em;float:right;font-size:20px;font-weight:700;color:#FFF;-webkit-text-shadow:0 1px 0 #fff;text-shadow:0 1px 0 #fff;opacity:.8;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=80);filter:alpha(opacity=80);line-height:1}.toast-close-button:focus,.toast-close-button:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.4;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=40);filter:alpha(opacity=40)}.rtl .toast-close-button{left:-.3em;float:left;right:.3em}button.toast-close-button{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none}.toast-top-center{top:0;right:0;width:100%}.toast-bottom-center{bottom:0;right:0;width:100%}.toast-top-full-width{top:0;right:0;width:100%}.toast-bottom-full-width{bottom:0;right:0;width:100%}.toast-top-left{top:12px;left:12px}.toast-top-right{top:12px;right:12px}.toast-bottom-right{right:12px;bottom:12px}.toast-bottom-left{bottom:12px;left:12px}#toast-container{position:fixed;z-index:999999;pointer-events:none}#toast-container *{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}#toast-container>div{position:relative;pointer-events:auto;overflow:hidden;margin:0 0 6px;padding:15px 15px 15px 50px;width:300px;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;background-position:15px center;background-repeat:no-repeat;-moz-box-shadow:0 0 12px #999;-webkit-box-shadow:0 0 12px #999;box-shadow:0 0 12px #999;color:#FFF;opacity:.8;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=80);filter:alpha(opacity=80)}#toast-container>div.rtl{direction:rtl;padding:15px 50px 15px 15px;background-position:right 15px center}#toast-container>div:hover{-moz-box-shadow:0 0 12px #000;-webkit-box-shadow:0 0 12px #000;box-shadow:0 0 12px #000;opacity:1;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=100);filter:alpha(opacity=100);cursor:pointer}#toast-container>.toast-info{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII=)!important}#toast-container>.toast-error{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII=)!important}#toast-container>.toast-success{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg==)!important}#toast-container>.toast-warning{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII=)!important}#toast-container.toast-bottom-center>div,#toast-container.toast-top-center>div{width:300px;margin-left:auto;margin-right:auto}#toast-container.toast-bottom-full-width>div,#toast-container.toast-top-full-width>div{width:96%;margin-left:auto;margin-right:auto}.toast{background-color:#030303}.toast-success{background-color:#51A351}.toast-error{background-color:#BD362F}.toast-info{background-color:#2F96B4}.toast-warning{background-color:#F89406}.toast-progress{position:absolute;left:0;bottom:0;height:4px;background-color:#000;opacity:.4;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=40);filter:alpha(opacity=40)}@media all and (max-width:240px){#toast-container>div{padding:8px 8px 8px 50px;width:11em}#toast-container>div.rtl{padding:8px 50px 8px 8px}#toast-container .toast-close-button{right:-.2em;top:-.2em}#toast-container .rtl .toast-close-button{left:-.2em;right:.2em}}@media all and (min-width:241px) and (max-width:480px){#toast-container>div{padding:8px 8px 8px 50px;width:18em}#toast-container>div.rtl{padding:8px 50px 8px 8px}#toast-container .toast-close-button{right:-.2em;top:-.2em}#toast-container .rtl .toast-close-button{left:-.2em;right:.2em}}@media all and (min-width:481px) and (max-width:768px){#toast-container>div{padding:15px 15px 15px 50px;width:25em}#toast-container>div.rtl{padding:15px 50px 15px 15px}} \ No newline at end of file diff --git a/public/index.html b/public/index.html index 4d231c692..7d1326c36 100644 --- a/public/index.html +++ b/public/index.html @@ -14,6 +14,7 @@ + @@ -41,6 +42,7 @@ + @@ -61,6 +63,7 @@ + SillyTavern @@ -368,12 +371,14 @@
- +
+ +
- Display a breakdown of the tokens used in the request. + Enable this if the streaming doesn't work with your proxy.
@@ -1018,6 +1023,9 @@
Get it here: Register
Enter 0000000000 to use anonymous mode.
+
+ View my Kudos +
@@ -1156,7 +1164,7 @@
- +
For privacy reasons, your API key will be hidden after you reload the page.
@@ -1287,7 +1295,7 @@
@@ -1295,12 +1303,20 @@
+
+ +
+ +
+
@@ -1339,6 +1355,21 @@ +
+

+ Context Templates +

+
+ + + + +
+

Pygmalion Formatting

- - - - - - - -
-
-
@@ -1459,7 +1472,7 @@ -
+
+
@@ -1773,7 +1795,7 @@
Blacklisted words
- +
Blacklisted word count to swipe
@@ -1909,7 +1931,7 @@ - +
 
@@ -2193,6 +2215,58 @@ +
+
+

Context Template Editor

+ +
+
+ Substitution Parameters +
+
+
+ Click to copy. +
    +
  • {{char}} - current character name
  • +
  • {{user}} - current user name
  • +
  • {{description}} - character description
  • +
  • {{scenario}} - character or group scenario
  • +
  • {{personality}} - character personality
  • +
  • {{mesExamples}} - message examples
  • +
  • {{wiBeforeCharacter}} - activated World Info entries (Before Char)
  • +
  • {{wiAfterCharacter}} - activated World Info entries (After Char)
  • +
  • {{instructSystemPrompt}} - system prompt (Instruct mode only)
  • +
+
+
+
+
+ Story String Template +
+ +
+ Lines containing parameters resolving to an empty value will be removed from the template string. +
+
+
+
+ Chat Injections + +
+
+
+
+
+ +
+
+ + + +
+
@@ -2380,7 +2454,7 @@
-
+
@@ -2389,7 +2463,7 @@
-
+
${characterName}
@@ -2497,11 +2571,6 @@
-
@@ -2513,7 +2582,7 @@
-
+
@@ -2532,7 +2601,7 @@ - Open Author's Note + Author's Note Show/Hide @@ -2575,6 +2644,15 @@
+ + \ No newline at end of file diff --git a/public/instruct/Alpaca.json b/public/instruct/Alpaca.json index 5c326bdba..df538760c 100644 --- a/public/instruct/Alpaca.json +++ b/public/instruct/Alpaca.json @@ -5,5 +5,6 @@ "stop_sequence": "", "input_sequence": "### Instruction:", "output_sequence": "### Response:", + "separator_sequence": "", "wrap": true } \ No newline at end of file diff --git a/public/instruct/Koala.json b/public/instruct/Koala.json index 48a8abd67..36b21637e 100644 --- a/public/instruct/Koala.json +++ b/public/instruct/Koala.json @@ -1,9 +1,10 @@ { "name": "Koala", "system_prompt": "Write {{char}}'s next reply in a fictional roleplay chat between {{user}} and {{char}}.\n", - "system_sequence": "BEGINNING OF CONVERSATION:", + "system_sequence": "BEGINNING OF CONVERSATION: ", "stop_sequence": "", "input_sequence": "USER: ", "output_sequence": "GPT: ", + "separator_sequence": "", "wrap": false } \ No newline at end of file diff --git a/public/instruct/Metharme.json b/public/instruct/Metharme.json index 31be23535..82827211a 100644 --- a/public/instruct/Metharme.json +++ b/public/instruct/Metharme.json @@ -5,5 +5,6 @@ "stop_sequence": "", "input_sequence": "<|user|>", "output_sequence": "<|model|>", + "separator_sequence": "", "wrap": false } \ No newline at end of file diff --git a/public/instruct/Vicuna 1.0.json b/public/instruct/Vicuna 1.0.json index ce5a11d93..2c27e75d2 100644 --- a/public/instruct/Vicuna 1.0.json +++ b/public/instruct/Vicuna 1.0.json @@ -5,5 +5,6 @@ "stop_sequence": "", "input_sequence": "### Human:", "output_sequence": "### Assistant:", + "separator_sequence": "", "wrap": true } \ No newline at end of file diff --git a/public/instruct/Vicuna 1.1.json b/public/instruct/Vicuna 1.1.json index 365f0f0ee..e35d88970 100644 --- a/public/instruct/Vicuna 1.1.json +++ b/public/instruct/Vicuna 1.1.json @@ -5,5 +5,6 @@ "stop_sequence": "", "input_sequence": "USER: ", "output_sequence": "ASSISTANT: ", - "wrap": true + "separator_sequence": "", + "wrap": false } \ No newline at end of file diff --git a/public/instruct/WizardLM.json b/public/instruct/WizardLM.json index 29fc6dcce..aa83c1a64 100644 --- a/public/instruct/WizardLM.json +++ b/public/instruct/WizardLM.json @@ -3,7 +3,8 @@ "system_prompt": "Write {{char}}'s next reply in a fictional roleplay chat between {{user}} and {{char}}.\n", "system_sequence": "", "stop_sequence": "", - "input_sequence": "### Instruction:", + "input_sequence": "", "output_sequence": "### Response:", + "separator_sequence": "", "wrap": true } \ No newline at end of file diff --git a/public/notes/content.md b/public/notes/content.md index f0920c276..e2b64c967 100644 --- a/public/notes/content.md +++ b/public/notes/content.md @@ -16,7 +16,7 @@ Usually it all takes 200-350 tokens. For most Kobold's models the easiest way is to use a free form for description, and in each sentence it is desirable to specify the name of the character. -The entire description should be in one line without hyphenation. +The entire description should be in one line without hyphenation. For example: @@ -50,11 +50,11 @@ This is because every AI model has a limit to the amount of context it can proce This is the information that gets sent to the AI each time you ask it to generate a response: -* Character definitions -* Chat history -* Author's Notes -* Special Format strings -* [bracket commands] +* Character definitions +* Chat history +* Author's Notes +* Special Format strings +* [bracket commands] SillyTavern automatically calculates the best way to allocate the available context tokens before sending the information to the AI model. @@ -62,23 +62,23 @@ SillyTavern automatically calculates the best way to allocate the available cont These will always be sent to the AI with every generation request: -* Character Name (keep the name short! Sent at the start of EVERY Character message) -* Character Description Box -* Character Personality Box -* Scenario Box +* Character Name (keep the name short! Sent at the start of EVERY Character message) +* Character Description Box +* Character Personality Box +* Scenario Box ### What parts of a Character's Definitions are NOT permanent? -* The first message box - only sent once at the start of the chat. -* Example messages box - only kept until chat history fills up the context (optionally these can be forced to be kept in context) +* The first message box - only sent once at the start of the chat. +* Example messages box - only kept until chat history fills up the context (optionally these can be forced to be kept in context) ### Popular AI Model Context Token Limits -* Older models below 6B parameters - 1024 -* Pygmalion 6B - 2048 -* Poe.com (Claude-instant or ChatGPT) - 2048 -* OpenAI ChatGPT - 4000-ish? -* OpenAI GPT-4 - 8000? +* Older models below 6B parameters - 1024 +* Pygmalion 6B - 2048 +* Poe.com (Claude-instant or ChatGPT) - 2048 +* OpenAI ChatGPT - 4000-ish? +* OpenAI GPT-4 - 8000? ### Personality summary @@ -96,16 +96,15 @@ Another example: ### First message -The First Message is an important thing that sets exactly how and in what style the character will communicate. +The First Message is an important thing that sets exactly how and in what style the character will communicate. -It is desirable that the character's first message be long, so that later it would be less likely that the character would respond in with very short messages. +It is desirable that the character's first message be long, so that later it would be less likely that the character would respond in with very short messages. You can also use asterisks ** to describe the character's actions. For example: -`*I noticed you came inside, I walked up and stood right in front of you* Welcome. I'm glad to see you here. *I said with toothy smug sunny smile looking you straight in the eye* What brings you...` - +`*I noticed you came inside, I walked up and stood right in front of you* Welcome. I'm glad to see you here. *I said with toothy smug sunny smile looking you straight in the eye* What brings you...` ### Examples of dialogue @@ -117,13 +116,13 @@ Example: ``` -{{user}}: Hi Aqua, I heard you like to spend time in the pub. -{{char}}: *excitedly* Oh my goodness, yes! I just love spending time at the pub! It's so much fun to talk to all the adventurers and hear about their exciting adventures! And you are? -{{user}}: I'm a new here and I wanted to ask for your advice. -{{char}}: *giggles* Oh, advice! I love giving advice! And in gratitude for that, treat me to a drink! *gives signals to the bartender* +{{user}}: Hi Aqua, I heard you like to spend time in the pub. +{{char}}: *excitedly* Oh my goodness, yes! I just love spending time at the pub! It's so much fun to talk to all the adventurers and hear about their exciting adventures! And you are? +{{user}}: I'm a new here and I wanted to ask for your advice. +{{char}}: *giggles* Oh, advice! I love giving advice! And in gratitude for that, treat me to a drink! *gives signals to the bartender* -{{user}}: Hello +{{user}}: Hello {{char}}: *excitedly* Hello there, dear! Are you new to Axel? Don't worry, I, Aqua the goddess of water, am here to help you! Do you need any assistance? And may I say, I look simply radiant today! *strikes a pose and looks at you with puppy eyes* ``` @@ -135,8 +134,10 @@ Circumstances and context of the dialogue. _A list of tags that are replaced when sending to generate:_ -1. {{user}} and <USER> are replaced by the User's Name +1. {{user}} and <USER> are replaced by the User's Name 2. {{char}} and <BOT> are replaced by the Character's Name +3. {{time}} is replaced with the current system time. +4. {{date}} is replaced with the current system date. ### Favorite Character @@ -162,7 +163,7 @@ _It is important to note that while World Info helps guide the AI towards your d #### Key -A list of keywords that trigger the activation of a World Info entry. +A list of keywords that trigger the activation of a World Info entry. Keys are not case-sensitive by default (this is [configurable](#casesensitivekeys)). #### Secondary Key @@ -217,7 +218,7 @@ Entries inserted by direct mentioning of their keys have higher priority than th **Entries can activate other entries by mentioning their keywords in the content text.** -For example, if your World Info contains two entries: +For example, if your World Info contains two entries: ``` Entry #1 @@ -233,6 +234,14 @@ Content: Rufus is a dog. **Both** of them will be pulled into the context if the message text mentions **just Bessie**. +### Case-sensitive keys + +**To get pulled into the context, entry keys need to match the case as they are defined in the World Info entry.** + +This is useful when your keys are common words or parts of common words. + +For example, when this setting is active, keys 'rose' and 'Rose' will be treated differently, depending on the inputs. + ## KoboldAI ### Basic Settings @@ -257,7 +266,7 @@ The maximum amount of tokens that the AI will generate to respond. One word is a #### Context size -How much will the AI remember. Context size also affects the speed of generation. +How much will the AI remember. Context size also affects the speed of generation. _Important_: The setting of Context Size in SillyTavern GUI overrides the setting for KoboldAI GUI @@ -322,10 +331,10 @@ They are created by training the AI with a special type of prompt using a collec To get a NovelAI API key, follow these instructions: -1. Go to the NovelAI website and Login. -2. Create a new story, or open an existing story. -3. Open the Network Tools on your web browser. (For Chrome or Firefox, you do this by pressing Ctrl+Shift+I, then switching to the Network tab.) -4. Generate something. You should see two requests to [api.novelai.net/ai/generate-stream](http://api.novelai.net/ai/generate-stream), which might look something like this: +1. Go to the NovelAI website and Login. +2. Create a new story, or open an existing story. +3. Open the Network Tools on your web browser. (For Chrome or Firefox, you do this by pressing Ctrl+Shift+I, then switching to the Network tab.) +4. Generate something. You should see two requests to [api.novelai.net/ai/generate-stream](http://api.novelai.net/ai/generate-stream), which might look something like this: ![1.png](1.png) @@ -339,7 +348,7 @@ The long string (after "Bearer", not including it) is your API key. ### Settings -The files with the settings are here (SillyTavern\public\NovelAI Settings). +The files with the settings are here (SillyTavern\public\NovelAI Settings). You can also manually add your own settings files. #### Temperature @@ -366,7 +375,7 @@ The range of influence of Repetition penalty in tokens. If your subscription tier is Paper, Tablet or Scroll use only Euterpe model otherwise you can not get an answer from NovelAI API. -## OpenAI +## OpenAI ### API key @@ -385,11 +394,14 @@ _Lost API keys can't be restored! Make sure to keep it safe!_ **How to get your access token / cookie:** -1. Login to [poe.com](https://poe.com) -2. Open browser DevTools (F12) and navigate to "Application" tab -3. Find a _p-b_ cookie for poe.com domain and copy its value -4. Paste cookie value to the box below and click "Connect" -5. Select a character and start chatting +1. Login to [poe.com](https://poe.com) +2. Open browser DevTools (F12) and navigate to "Application" tab. +3. Type any message into the poe.com chat, and get a response from the AI. +4. Find the 'Cookie' section on the left side of Dev Tools 'Application' tab, expand it +5. Click "" listing inside the Cookies section. +6. Look to the right for the listing of _p-b_ and copy its Value. +7. Paste the cookie value into the Poe API connection URL box, and click "Connect". +8. Select a character and start chatting ## Anchors @@ -419,7 +431,7 @@ Write one reply in internet RP style for {{char}}. Be verbose and creative. Provides ready-made presets with prompts and sequences for some well-known instruct models. -*Changing a preset resets your system prompt to default!* +_Changing a preset resets your system prompt to default!_ #### Input Sequence @@ -433,6 +445,10 @@ Text added before the character's reply. Text added before the system prompt. +#### Separator Sequence + +Text added after the character reply to separate the chat history logs. + #### Stop Sequence Text that denotes the end of the reply. Will be trimmed from the output text. @@ -441,7 +457,7 @@ Text that denotes the end of the reply. Will be trimmed from the output text. If enabled, prepend character and user names to chat history logs after inserting the sequences. -*Always enabled for group chats!* +_Always enabled for group chats!_ #### Wrap Sequences with Newline @@ -457,7 +473,7 @@ To import Character.AI chats, use this tool: [https://github.com/0x000011b/chara **Important: This section doesn't apply to OpenAI API. SillyTavern will always use a matching tokenizer for OpenAI models.** -A tokenizer is a tool that breaks down a piece of text into smaller units called tokens. These tokens can be individual words or even parts of words, such as prefixes, suffixes, or punctuation. A rule of thumb is that one token generally corresponds to 3~4 characters of text. +A tokenizer is a tool that breaks down a piece of text into smaller units called tokens. These tokens can be individual words or even parts of words, such as prefixes, suffixes, or punctuation. A rule of thumb is that one token generally corresponds to 3~4 characters of text. SillyTavern can use the following tokenizers while forming a request to the AI backend: @@ -470,7 +486,7 @@ SillyTavern can use the following tokenizers while forming a request to the AI b **Important: This section doesn't apply to OpenAI API. SillyTavern will always use a matching tokenizer for OpenAI models.** -SillyTavern cannot use a proper tokenizer provided by the model running on a remote instance of KoboldAI or Oobabooga's TextGen, so all token counts assumed during prompt generation are estimated based on the selected [tokenizer](#Tokenizer) type. +SillyTavern cannot use a proper tokenizer provided by the model running on a remote instance of KoboldAI or Oobabooga's TextGen, so all token counts assumed during prompt generation are estimated based on the selected [tokenizer](#tokenizer) type. Since the results of tokenization can be inaccurate on context sizes close to the model-defined maximum, some parts of the prompt may be trimmed or dropped, which may negatively affect the coherence of character definitions. @@ -490,24 +506,24 @@ Overrides the default separators controlled by "Disable example chats formatting #### Disable description formatting -`**NAME's Persona:** `won't be prepended to the content of your character's Description box. +`**NAME's Persona:**`won't be prepended to the content of your character's Description box. #### Disable scenario formatting -`**Scenario:** `won't be prepended to the content of your character's Scenario box. +`**Scenario:**`won't be prepended to the content of your character's Scenario box. #### Disable personality formatting -`**Personality:** `won't be prepended to the content of your character's Personality box. +`**Personality:**`won't be prepended to the content of your character's Personality box. #### Disable example chats formatting -`` won't be added at the beginning of each example message block. +`` won't be added at the beginning of each example message block. _(If custom separator is not set)_ #### Disable chat start formatting -`` won't be added between the character card and the chat log. +`` won't be added between the character card and the chat log. _(If custom separator is not set)_ #### Always add character's name to prompt @@ -522,20 +538,20 @@ Has no effect. #### Disable scenario formatting -`**Circumstances and context of the dialogue:** `won't be prepended to the content of your character's Scenario box. +`**Circumstances and context of the dialogue:**`won't be prepended to the content of your character's Scenario box. #### Disable personality formatting -`**NAME's personality:** `won't be prepended to the content of your character's Personality box. +`**NAME's personality:**`won't be prepended to the content of your character's Personality box. #### Disable example chats formatting -`This is how **Character** should talk` won't be added at the beginning of each example message block. +`This is how **Character** should talk` won't be added at the beginning of each example message block. _(If custom separator is not set)_ #### Disable chat start formatting -`Then the roleplay chat between **User** and **Character** begins` won't be added between the character card and the chat log. +`Then the roleplay chat between **User** and **Character** begins` won't be added between the character card and the chat log. _(If custom separator is not set)_ #### Always add character's name to prompt @@ -544,7 +560,7 @@ Appends character's name to the prompt to force the model to complete the messag ``` ** OTHER CONTEXT HERE ** -Character: +Character: ``` ## Group Chats @@ -577,31 +593,31 @@ Characters are drafted based on the order they are presented in group members li ## Multigen -*This feature provides a pseudo-streaming functionality which conflicts with token streaming. When Multigen is enabled and generation API supports streaming, only Multigen streaming will be used.* +_This feature provides a pseudo-streaming functionality which conflicts with token streaming. When Multigen is enabled and generation API supports streaming, only Multigen streaming will be used._ SillyTavern tries to create faster and longer responses by chaining the generation using smaller batches. -### Default settings: +### Default settings First batch = 50 tokens Next batches = 30 tokens -### Algorithm: +### Algorithm 1. Generate the first batch (if amount of generation setting is more than batch length). 2. Generate next batch of tokens until one of the stopping conditions is reached. 3. Append the generated text to the next cycle's prompt. -### Stopping conditions: +### Stopping conditions 1. Generated enough text. 2. Character starts speaking for You. 3. <|endoftext|> token reached. 4. No text generated. -5. Stop sequence generated. (Instruct mode only) +5. Stop sequence generated. (Instruct mode only) -## User Settings +## User Settings ### Message Sound @@ -620,13 +636,15 @@ Enables math formulas rendering using the [showdown-katex](https://obedm503.gith The following formatting rules are supported: #### LaTeX syntax + ``` $$ formula goes here $$ ``` #### Asciimath syntax + ``` -$ formula goes here $ +formula goes here $ ``` -More information: [KaTeX](https://katex.org/) \ No newline at end of file +More information: [KaTeX](https://katex.org/) diff --git a/public/script.js b/public/script.js index c5438d457..71f78a498 100644 --- a/public/script.js +++ b/public/script.js @@ -49,6 +49,7 @@ import { editGroup, deleteGroupChat, renameGroupChat, + importGroupChat, } from "./scripts/group-chats.js"; import { @@ -106,7 +107,7 @@ import { setPoeOnlineStatus, } from "./scripts/poe.js"; -import { debounce, delay, restoreCaretPosition, saveCaretPosition, end_trim_to_sentence } from "./scripts/utils.js"; +import { debounce, delay, restoreCaretPosition, saveCaretPosition, end_trim_to_sentence, countOccurrences, isOdd } from "./scripts/utils.js"; import { extension_settings, loadExtensionSettings, runGenerationInterceptors } from "./scripts/extensions.js"; import { executeSlashCommands, getSlashCommandsHelp, registerSlashCommand } from "./scripts/slash-commands.js"; import { @@ -227,7 +228,6 @@ let safetychat = [ ]; let chat_create_date = 0; -let prev_selected_char = null; const default_ch_mes = "Hello"; let count_view_mes = 0; let mesStr = ""; @@ -292,12 +292,22 @@ const system_messages = { is_system: true, is_name: true, mes: [ - 'Hi there! The following chat formatting commands are supported:', - '
    ', - '
  1. {{text}} – sets a permanent behavioral bias for the AI
  2. ', - '
  3. {{}} – removes any active character bias
  4. ', - '
', - ].join('') + `Hi there! The following chat formatting commands are supported: +
    +
  • {​{text}​} - sets a one-time behavioral bias for the AI. Resets when you send the next message. +
  • +
+ Hotkeys/Keybinds: +
    +
  • Up = Edit last message in chat
  • +
  • Ctrl+Up = Edit last USER message in chat
  • +
  • Left = swipe left
  • +
  • Right = swipe right (NOTE: swipe hotkeys are disabled when chatbar has something typed into it)
  • +
  • Ctrl+Left = view locally stored variables (in the browser console window)
  • +
  • Enter (with chat bar selected) = send your message to AI
  • +
  • Ctrl+Enter = Regenerate the last AI response
  • +
` + ] }, welcome: { @@ -306,42 +316,42 @@ const system_messages = { is_user: false, is_name: true, mes: [ - '

Welcome to SillyTavern!

', - '
', - '

Want to Update to the latest version?

', - "Read the
instructions here. Also located in your installation's base folder", - '
', - '

In order to begin chatting:

', - '
    ', - '
  1. Connect to one of the supported generation APIs (the plug icon)
  2. ', - '
  3. Create or pick a character from the list (the top-right namecard icon)
  4. ', - '
', - '
', - '

Where to download more characters?

', - '(Not endorsed, your discretion is advised)', - '
    ', - '
  1. Pygmalion AI Discord
  2. ', - '
  3. CharacterHub (NSFW)
  4. ', - '
', - '
', - '

Where can I get help?

', - 'Before going any further, check out the following resources:', - '
    ', - '
  1. Introduction to SillyTavern
  2. ', - '
  3. SillyTavern FAQ
  4. ', - '
  5. SillyTavern Guidebook
  6. ', - '
  7. Extras API Docs
  8. ', - '
  9. Pygmalion AI Docs
  10. ', - '
', - 'Type /? in any chat to get help on message formatting commands.', - '
', - '

Still have questions or suggestions left?

', - 'SillyTavern Community Discord', - '
', - 'Post a GitHub issue.', - '
', - 'Contact the developers.', - ].join('') + `

Welcome to SillyTavern!

+
+

Want to Update to the latest version?

+ Read the instructions here. Also located in your installation's base folder +
+

In order to begin chatting:

+
    +
  1. Connect to one of the supported generation APIs (the plug icon)
  2. +
  3. Create or pick a character from the list (the top-right namecard icon)
  4. +
+
+

Where to download more characters?

+ (Not endorsed, your discretion is advised) +
    +
  1. Pygmalion AI Discord
  2. +
  3. CharacterHub (NSFW)
  4. +
+
+

Where can I get help?

+ Before going any further, check out the following resources: +
    +
  1. Introduction to SillyTavern
  2. +
  3. SillyTavern FAQ
  4. +
  5. SillyTavern Guidebook
  6. +
  7. Extras API Docs
  8. +
  9. Pygmalion AI Docs
  10. +
+ Type /? in any chat to get help on message formatting commands. +
+

Still have questions or suggestions left?

+ SillyTavern Community Discord +
+ Post a GitHub issue. +
+ Contact the developers. + `].join('') }, group: { name: systemUserName, @@ -410,7 +420,7 @@ async function getClientVersion() { let displayVersion = `SillyTavern ${data.pkgVersion}`; if (data.gitRevision && data.gitBranch) { - displayVersion += ` '${data.gitBranch}' (${data.gitRevision})`; + displayVersion += ` '${data.gitBranch}'(${data.gitRevision})`; } $('#version_display').text(displayVersion); @@ -833,13 +843,17 @@ async function getBackgrounds() { for (const bg of getData) { const thumbPath = getThumbnailUrl('bg', bg); $("#bg_menu_content").append( - `
-
-
` + `
+
+
+ ${bg + .replace('.png', '') + .replace('.jpg', '') + .replace('.webp', '')} +
+
` ); } - //var aa = JSON.parse(getData[0]); - //const load_ch_coint = Object.getOwnPropertyNames(getData); } } async function isColab() { @@ -1022,6 +1036,11 @@ function messageFormatting(mes, ch_name, isSystem, isUser) { }); } + // Hides bias from empty messages send with slash commands + if (isSystem) { + mes = mes.replace(/{{(\*?.*\*?)}}/g, ""); + } + if (!power_user.allow_name2_display && ch_name && !isUser && !isSystem) { mes = mes.replaceAll(`${ch_name}:`, ""); } @@ -1155,7 +1174,7 @@ function addOneMessage(mes, { type = "normal", insertAfter = null, scroll = true newMessage.data("isSystem", isSystem); if (isSystem) { - newMessage.find(".mes_edit").hide(); + // newMessage.find(".mes_edit").hide(); newMessage.find(".mes_prompt").hide(); //don't need prompt button for sys } @@ -1259,6 +1278,8 @@ function substituteParams(content, _name1, _name2) { content = content.replace(/{{char}}/gi, _name2); content = content.replace(//gi, _name1); content = content.replace(//gi, _name2); + content = content.replace(/{{time}}/gi, moment().format('LT')); + content = content.replace(/{{date}}/gi, moment().format('LL')); return content; } @@ -1315,7 +1336,7 @@ function processCommands(message, type) { return result.interrupt; } -function sendSystemMessage(type, text) { +function sendSystemMessage(type, text, extra = {}) { const systemMessage = system_messages[type]; if (!systemMessage) { @@ -1336,6 +1357,7 @@ function sendSystemMessage(type, text) { newMessage.extra = {}; } + newMessage.extra = Object.assign(newMessage.extra, extra); newMessage.extra.type = type; chat.push(newMessage); @@ -1343,7 +1365,7 @@ function sendSystemMessage(type, text) { is_send_press = false; } -function extractMessageBias(message) { +export function extractMessageBias(message) { if (!message) { return null; } @@ -1357,15 +1379,13 @@ function extractMessageBias(message) { found.push(curMatch[1].trim()); } - if (!found.length) { - // cancels a bias - if (message.includes('{{') && message.includes('}}')) { - return ''; - } - return null; + let biasString = ''; + + if (found.length) { + biasString = ` ${found.join(" ")}` } - return ` ${found.join(" ")} `; + return biasString; } function cleanGroupMessage(getMessage) { @@ -1513,6 +1533,12 @@ class StreamingProcessor { let isName = result.this_mes_is_name; processedText = result.getMessage; + // Predict unbalanced asterisks during streaming + if (!isFinal && isOdd(countOccurrences(processedText, '*'))) { + // Add asterisk at the end to balance it + processedText = processedText.trimEnd() + '*'; + } + if (isImpersonate) { $('#send_textarea').val(processedText).trigger('input'); } @@ -1661,8 +1687,6 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, setGenerationProgress(0); tokens_already_generated = 0; generation_started = new Date(); - - await runGenerationInterceptors(); const isImpersonate = type == "impersonate"; const isInstruct = power_user.instruct.enabled; @@ -1689,7 +1713,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, } if (main_api == 'textgenerationwebui' && textgenerationwebui_settings.streaming && !textgenerationwebui_settings.streaming_url) { - callPopup('Streaming URL is not set. Look it up in the console window when starting TextGen Web UI', 'text'); + toastr.error('Streaming URL is not set. Look it up in the console window when starting TextGen Web UI'); is_send_press = false; return; } @@ -1699,8 +1723,6 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, return; } - streamingProcessor = isStreamingEnabled() ? new StreamingProcessor(type, force_name2) : false; - // Hide swipes on either multigen or real streaming if (isStreamingEnabled() || isMultigenEnabled()) { hideSwipeButtons(); @@ -1741,21 +1763,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, deactivateSendButtons(); - let promptBias = null; - let messageBias = extractMessageBias(textareaText); - - // gets bias of the latest message where it was applied - for (let mes of chat.slice().reverse()) { - if (mes && mes.is_user && mes.extra && mes.extra.bias) { - if (mes.extra.bias.trim().length > 0) { - promptBias = mes.extra.bias; - } - break; - } - } - - // bias from the latest message is top priority// - promptBias = messageBias ?? promptBias ?? ''; + let { messageBias, promptBias } = getBiasStrings(textareaText); //********************************* //PRE FORMATING STRING @@ -1763,7 +1771,13 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, //for normal messages sent from user.. if (textareaText != "" && !automatic_trigger && type !== 'quiet') { - sendMessageAsUser(textareaText, messageBias); + // If user message contains no text other than bias - send as a system message + if (messageBias && replaceBiasMarkup(textareaText).trim().length === 0) { + sendSystemMessage(system_message_types.GENERIC, ' ', { bias: messageBias }); + } + else { + sendMessageAsUser(textareaText, messageBias); + } } //////////////////////////////////// const scenarioText = chat_metadata['scenario'] || characters[this_chid].scenario; @@ -1796,6 +1810,8 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, if (type === 'swipe') { coreChat.pop(); } + + await runGenerationInterceptors(coreChat); console.log(`Core/all messages: ${coreChat.length}/${chat.length}`); if (main_api === 'openai') { @@ -1816,8 +1832,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, storyString += appendToStoryString(Scenario, power_user.disable_scenario_formatting ? '' : 'Circumstances and context of the dialogue: '); } - // Pygmalion does that anyway - if (power_user.always_force_name2 && !is_pygmalion) { + if (promptBias || power_user.always_force_name2 || is_pygmalion) { force_name2 = true; } @@ -1879,7 +1894,6 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, storyString, examplesString, chatString, - promptBias, allAnchors, quiet_prompt, ].join('').replace(/\r/gm, ''); @@ -1901,15 +1915,14 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, } chatString = item + chatString; - if (canFitMessages()) { //(The number of tokens in the entire promt) need fix, it must count correctly (added +120, so that the description of the character does not hide) - //if (is_pygmalion && i == chat2.length-1) item='\n'+item; + if (canFitMessages()) { arrMes[arrMes.length] = item; - } else { break; - } - await delay(1); //For disable slow down (encode gpt-2 need fix) + + // Prevent UI thread lock on tokenization + await delay(1); } if (main_api !== 'openai') { @@ -1932,6 +1945,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, let mesSend = []; console.log('calling runGenerate'); + streamingProcessor = isStreamingEnabled() ? new StreamingProcessor(type, force_name2) : false; await runGenerate(); async function runGenerate(cycleGenerationPromt = '') { @@ -1964,14 +1978,6 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, } } if (is_pygmalion && !isInstruct) { - if (i === arrMes.length - 1 && item.trim().startsWith(name1 + ":")) {//for add name2 when user sent - item = item + name2 + ":"; - } - if (i === arrMes.length - 1 && !item.trim().startsWith(name1 + ":")) {//for add name2 when continue - if (textareaText == "") { - item = item + '\n' + name2 + ":"; - } - } if (item.trim().startsWith(name1)) { item = item.replace(name1 + ':', 'You:'); } @@ -2013,35 +2019,46 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, const isBottom = j === mesSend.length - 1; mesSendString += mesSend[j]; - // Add quiet generation prompt at depth 0 - if (isBottom && quiet_prompt && quiet_prompt.length) { - const name = is_pygmalion ? 'You' : name1; - const quietAppend = isInstruct ? formatInstructModeChat(name, quiet_prompt, false, true) : `\n${name}: ${quiet_prompt}`; - mesSendString += quietAppend; - } - - if (isInstruct && isBottom && tokens_already_generated === 0) { - const name = isImpersonate ? (is_pygmalion ? 'You' : name1) : name2; - mesSendString += formatInstructModePrompt(name, isImpersonate); - } - - if (!isInstruct && isImpersonate && isBottom && tokens_already_generated === 0) { - const name = is_pygmalion ? 'You' : name1; - if (!mesSendString.endsWith('\n')) { - mesSendString += '\n'; - } - mesSendString += name + ':'; - } - - if (force_name2 && isBottom && tokens_already_generated === 0) { - if (!mesSendString.endsWith('\n')) { - mesSendString += '\n'; - } - mesSendString += name2 + ':'; + if (isBottom) { + mesSendString = modifyLastPromptLine(mesSendString); } } } + function modifyLastPromptLine(mesSendString) { + // Add quiet generation prompt at depth 0 + if (quiet_prompt && quiet_prompt.length) { + const name = is_pygmalion ? 'You' : name1; + const quietAppend = isInstruct ? formatInstructModeChat(name, quiet_prompt, false, true, false) : `\n${name}: ${quiet_prompt}`; + mesSendString += quietAppend; + } + + // Get instruct mode line + if (isInstruct && tokens_already_generated === 0) { + const name = isImpersonate ? (is_pygmalion ? 'You' : name1) : name2; + mesSendString += formatInstructModePrompt(name, isImpersonate, promptBias); + } + + // Get non-instruct impersonation line + if (!isInstruct && isImpersonate && tokens_already_generated === 0) { + const name = is_pygmalion ? 'You' : name1; + if (!mesSendString.endsWith('\n')) { + mesSendString += '\n'; + } + mesSendString += name + ':'; + } + + // Add character's name + if (!isInstruct && force_name2 && tokens_already_generated === 0) { + if (!mesSendString.endsWith('\n')) { + mesSendString += '\n'; + } + mesSendString += (`${name2}:${promptBias || ''}`); + } + + return mesSendString; + } + function checkPromtSize() { console.log('---checking Prompt size'); setPromtString(); @@ -2051,7 +2068,6 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, mesExmString, mesSendString, generatedPromtCache, - promptBias, allAnchors, quiet_prompt, ].join('').replace(/\r/gm, ''); @@ -2089,8 +2105,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, afterScenarioAnchor + mesExmString + mesSendString + - generatedPromtCache + - promptBias; + generatedPromtCache; if (zeroDepthAnchor && zeroDepthAnchor.length) { if (!isMultigenEnabled() || tokens_already_generated == 0) { @@ -2190,9 +2205,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, type: 'POST', // url: generate_url, // data: JSON.stringify(generate_data), - beforeSend: function () { - - }, + beforeSend: () => { }, cache: false, dataType: "json", contentType: "application/json", @@ -2361,33 +2374,52 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, } //rungenerate ends } else { //generate's primary loop ends, after this is error handling for no-connection or safety-id if (this_chid === undefined || this_chid === 'invalid-safety-id') { - //send ch sel - popup_type = 'char_not_selected'; - callPopup('

Сharacter is not selected

'); + toastr.warning('Сharacter is not selected'); } is_send_press = false; } //console.log('generate ending'); } //generate ends +function getBiasStrings(textareaText) { + let promptBias = ''; + let messageBias = extractMessageBias(textareaText); + + // gets bias of the latest message where it was applied + for (let mes of chat.slice().reverse()) { + if (mes && mes.extra && (mes.is_user || mes.is_system || mes.extra.type === system_message_types.NARRATOR)) { + if (mes.extra.bias && mes.extra.bias.trim().length > 0) { + promptBias = mes.extra.bias; + } + break; + } + } + + promptBias = messageBias || promptBias || ''; + return { messageBias, promptBias }; +} + function formatMessageHistoryItem(chatItem, isInstruct) { const isNarratorType = chatItem?.extra?.type === system_message_types.NARRATOR; - const characterName = selected_group ? chatItem.name : name2; + const characterName = (selected_group || chatItem.force_avatar) ? chatItem.name : name2; const itemName = chatItem.is_user ? chatItem['name'] : characterName; - const shouldPrependName = (chatItem.is_name || selected_group) && !isNarratorType; + const shouldPrependName = (chatItem.is_name || chatItem.force_avatar || selected_group) && !isNarratorType; let textResult = shouldPrependName ? `${itemName}: ${chatItem.mes}\n` : `${chatItem.mes}\n`; if (isInstruct) { - textResult = formatInstructModeChat(itemName, chatItem.mes, chatItem.is_user, isNarratorType); + textResult = formatInstructModeChat(itemName, chatItem.mes, chatItem.is_user, isNarratorType, chatItem.force_avatar); } - // replace bias markup - textResult = (textResult ?? '').replace(/{{(\*?.*\*?)}}/g, ''); + textResult = replaceBiasMarkup(textResult); return textResult; } +export function replaceBiasMarkup(str) { + return (str ?? '').replace(/{{(\*?.*\*?)}}/g, ''); +} + function sendMessageAsUser(textareaText, messageBias) { chat[chat.length] = {}; chat[chat.length - 1]['name'] = name1; @@ -2401,7 +2433,7 @@ function sendMessageAsUser(textareaText, messageBias) { console.log('checking bias'); chat[chat.length - 1]['extra']['bias'] = messageBias; } - //console.log('Generate calls addOneMessage'); + addOneMessage(chat[chat.length - 1]); } @@ -2414,7 +2446,7 @@ function getMaxContextSize() { if (novel_tier === 1) { this_max_context = 1024; } else { - this_max_context = 2048 - 60; //fix for fat tokens + this_max_context = 2048 - 60; //fix for fat tokens if (nai_settings.model_novel == 'krake-v2') { this_max_context -= 160; } @@ -2430,9 +2462,6 @@ function getMaxContextSize() { } function parseTokenCounts(counts, thisPromptBits) { - const breakdown_bar = $('#token_breakdown div:first-child'); - breakdown_bar.empty(); - const total = Object.values(counts).filter(x => !Number.isNaN(x)).reduce((acc, val) => acc + val, 0); thisPromptBits.push({ @@ -2446,22 +2475,6 @@ function parseTokenCounts(counts, thisPromptBits) { oaiConversationTokens: Object.entries(counts)[7][1], oaiTotalTokens: total, }); - - Object.entries(counts).forEach(([type, value]) => { - if (value === 0) { - return; - } - const percent_value = (value / total) * 100; - const color = uniqolor(type, { saturation: 50, lightness: 75, }).color; - const bar = document.createElement('div'); - bar.style.width = `${percent_value}%`; - bar.classList.add('token_breakdown_segment'); - bar.style.backgroundColor = color + 'AA'; - bar.style.borderColor = color + 'FF'; - bar.innerText = value; - bar.title = `${type}: ${percent_value.toFixed(2)}%`; - breakdown_bar.append(bar); - }); } function adjustChatsSeparator(mesSendString) { @@ -2714,7 +2727,7 @@ function promptItemize(itemizedPrompts, requestedMesId) {
World Info:
${worldInfoStringTokens}
-
+
Chat History:
${ActualChatHistoryTokens}
@@ -2801,7 +2814,7 @@ function promptItemize(itemizedPrompts, requestedMesId) {
World Info:
${worldInfoStringTokens}
-
+
Chat History:
${ActualChatHistoryTokens}
@@ -2997,11 +3010,6 @@ function extractMessageFromData(data) { if (main_api == 'textgenerationwebui') { getMessage = data.results[0].text; - if (getMessage == null || data.error) { - activateSendButtons(); - callPopup('

Got empty response from Text generation web UI. Try restarting the API with recommended options.

', 'text'); - return; - } } if (main_api == 'novel') { @@ -3408,6 +3416,7 @@ async function read_avatar_load(input) { await delay(durationSaveEdit); await fetch(getThumbnailUrl('avatar', formData.get('avatar_url')), { method: 'GET', + cache: 'no-cache', headers: { 'pragma': 'no-cache', 'cache-control': 'no-cache', @@ -3579,10 +3588,8 @@ function changeMainAPI() { // Hide common settings for OpenAI if (selectedVal == "openai") { $("#common-gen-settings-block").css("display", "none"); - //$("#token_breakdown").css("display", "flex"); } else { $("#common-gen-settings-block").css("display", "block"); - //$("#token_breakdown").css("display", "none"); } // Hide amount gen for poe if (selectedVal == "poe") { @@ -3857,6 +3864,7 @@ async function saveSettings(type) { world_info_depth: world_info_depth, world_info_budget: world_info_budget, world_info_recursive: world_info_recursive, + world_info_case_sensitive: world_info_case_sensitive, textgenerationwebui_settings: textgenerationwebui_settings, swipes: swipes, horde_settings: horde_settings, @@ -3900,22 +3908,30 @@ function messageEditAuto(div) { let mesBlock = div.closest(".mes_block"); var text = mesBlock.find(".edit_textarea").val().trim(); const bias = extractMessageBias(text); - chat[this_edit_mes_id]["mes"] = text; - if (chat[this_edit_mes_id]["swipe_id"] !== undefined) { - chat[this_edit_mes_id]["swipes"][chat[this_edit_mes_id]["swipe_id"]] = text; + const mes = chat[this_edit_mes_id]; + mes["mes"] = text; + if (mes["swipe_id"] !== undefined) { + mes["swipes"][mes["swipe_id"]] = text; } // editing old messages - if (!chat[this_edit_mes_id]["extra"]) { - chat[this_edit_mes_id]["extra"] = {}; + if (!mes["extra"]) { + mes["extra"] = {}; } - chat[this_edit_mes_id]["extra"]["bias"] = bias ?? null; + + if (mes.is_system || mes.is_user || mes.extra.type === system_message_types.NARRATOR) { + mes.extra.bias = bias ?? null; + } + else { + mes.extra.bias = null; + } + mesBlock.find(".mes_text").val(''); mesBlock.find(".mes_text").val(messageFormatting( text, this_edit_mes_chname, - chat[this_edit_mes_id].is_system, - chat[this_edit_mes_id].is_user, + mes.is_system, + mes.is_user, )); saveChatDebounced(); } @@ -3924,17 +3940,23 @@ function messageEditDone(div) { let mesBlock = div.closest(".mes_block"); var text = mesBlock.find(".edit_textarea").val().trim(); const bias = extractMessageBias(text); - chat[this_edit_mes_id]["mes"] = text; - if (chat[this_edit_mes_id]["swipe_id"] !== undefined) { - chat[this_edit_mes_id]["swipes"][chat[this_edit_mes_id]["swipe_id"]] = text; + const mes = chat[this_edit_mes_id]; + mes["mes"] = text; + if (mes["swipe_id"] !== undefined) { + mes["swipes"][mes["swipe_id"]] = text; } // editing old messages - if (!chat[this_edit_mes_id]["extra"]) { - chat[this_edit_mes_id]["extra"] = {}; + if (!mes.extra) { + mes.extra = {}; } - chat[this_edit_mes_id]["extra"]["bias"] = bias ?? null; + if (mes.is_system || mes.is_user || mes.extra.type === system_message_types.NARRATOR) { + mes.extra.bias = bias ?? null; + } + else { + mes.extra.bias = null; + } mesBlock.find(".mes_text").empty(); mesBlock.find(".mes_edit_buttons").css("display", "none"); @@ -3943,13 +3965,13 @@ function messageEditDone(div) { messageFormatting( text, this_edit_mes_chname, - chat[this_edit_mes_id].is_system, - chat[this_edit_mes_id].is_user, + mes.is_system, + mes.is_user, ) ); mesBlock.find(".mes_bias").empty(); mesBlock.find(".mes_bias").append(messageFormatting(bias)); - appendImageToMessage(chat[this_edit_mes_id], div.closest(".mes")); + appendImageToMessage(mes, div.closest(".mes")); addCopyToCodeBlocks(div.closest(".mes")); this_edit_mes_id = undefined; saveChatConditional(); @@ -3972,7 +3994,7 @@ async function getPastCharacterChats() { return data; } -async function displayPastChats() { +export async function displayPastChats() { $("#select_chat_div").empty(); const group = selected_group ? groups.find(x => x.id === selected_group) : null; @@ -4009,7 +4031,7 @@ async function displayPastChats() { $("#select_chat_div").append(template); - if (currentChat === fileName.replace(".jsonl", "")) { + if (currentChat === fileName.toString().replace(".jsonl", "")) { $("#select_chat_div").find(".select_chat_block:last").attr("highlight", true); } } @@ -4054,7 +4076,6 @@ async function getStatusNovel() { } } - function selectRightMenuWithAnimation(selectedMenuId) { const displayModes = { 'rm_info_block': 'flex', @@ -4075,13 +4096,7 @@ function selectRightMenuWithAnimation(selectedMenuId) { easing: animation_easing, complete: function () { }, }); - - - - // $(menu).find('#groupCurrentMemberListToggle').click(); - } - }) } @@ -4095,16 +4110,39 @@ function setRightTabSelectedClass(selectedButtonId) { }); } -function select_rm_info(text, charId = null) { - $("#rm_info_text").html("

" + text + "

"); +function select_rm_info(type, charId, previousCharId = null) { + if (!type) { + toastr.error(`Invalid process (no 'type')`); + return; + } + if (type === 'char_delete') { + toastr.warning(`Character Deleted: ${charId}`); + } + if (type === 'char_create') { + toastr.success(`Character Created: ${charId}`); + } + if (type === 'char_import') { + toastr.success(`Character Imported: ${charId}`); + } + + selectRightMenuWithAnimation('rm_characters_block'); + + if (type === 'char_import' || type === 'char_create') { + + //$(`#rm_characters_block [title="${charId + '.png'}"]`).scrollIntoView({ behavior: "smooth", block: "end" }); + const element = $(`#rm_characters_block [title="${charId + '.png'}"]`).get(0); + element.scrollIntoView({ behavior: 'smooth', block: 'end' }); + + $(`#rm_characters_block [title="${charId + '.png'}"]`).parent().addClass('flash animated'); + setTimeout(function () { + $(`#rm_characters_block [title="${charId + '.png'}"]`).parent().removeClass('flash animated'); + }, 5000); + } - selectRightMenuWithAnimation('rm_info_block'); setRightTabSelectedClass(); - prev_selected_char = charId; - - if (prev_selected_char) { - const newId = characters.findIndex((x) => x.name == prev_selected_char); + if (previousCharId) { + const newId = characters.findIndex((x) => x.avatar == previousCharId); if (newId >= 0) { this_chid = newId; } @@ -4211,21 +4249,11 @@ function select_rm_create() { } function select_rm_characters() { - restoreSelectedCharacter(); - menu_type = "characters"; selectRightMenuWithAnimation('rm_characters_block'); setRightTabSelectedClass('rm_button_characters'); } -function restoreSelectedCharacter() { - if (prev_selected_char) { - let newChId = characters.findIndex((x) => x.name == prev_selected_char); - $(`.character_select[chid="${newChId}"]`).trigger("click"); - prev_selected_char = null; - } -} - function setExtensionPrompt(key, value, position, depth) { extension_prompts[key] = { value, position, depth }; } @@ -4428,6 +4456,27 @@ export async function saveChatConditional() { } } +async function importCharacterChat(formData) { + await jQuery.ajax({ + type: "POST", + url: "/importchat", + data: formData, + beforeSend: function () { + }, + cache: false, + contentType: false, + processData: false, + success: async function (data) { + if (data.res) { + await displayPastChats(); + } + }, + error: function () { + $("#create_button").removeAttr("disabled"); + }, + }); +} + function updateViewMessageIds() { $('#chat').find(".mes").each(function (index, element) { $(element).attr("mesid", index); @@ -4479,7 +4528,7 @@ function setGenerationProgress(progress) { function isHordeGenerationNotAllowed() { if (main_api == "kobold" && horde_settings.use_horde && preset_settings == "gui") { - callPopup('GUI Settings preset is not supported for Horde. Please select another preset.', 'text'); + toastr.error('GUI Settings preset is not supported for Horde. Please select another preset.'); return true; } @@ -4487,7 +4536,7 @@ function isHordeGenerationNotAllowed() { } export function cancelTtsPlay() { - if (speechSynthesis) { + if ('speechSynthesis' in window) { speechSynthesis.cancel(); } } @@ -5163,7 +5212,7 @@ $(document).ready(function () { method: "POST", url: "/deletecharacter", beforeSend: function () { - select_rm_info("Character deleted"); + select_rm_info("char_delete", characters[this_chid].name); //$('#create_button').attr('value','Deleting...'); }, data: msg, @@ -5335,7 +5384,7 @@ $(document).ready(function () { $("#create_button").attr("value", "✅"); let oldSelectedChar = null; if (this_chid != undefined && this_chid != "invalid-safety-id") { - oldSelectedChar = characters[this_chid].name; + oldSelectedChar = characters[this_chid].avatar; } console.log(`new avatar id: ${html}`); @@ -5345,7 +5394,7 @@ $(document).ready(function () { $("#rm_info_block").transition({ opacity: 0, duration: 0 }); var $prev_img = $("#avatar_div_div").clone(); $("#rm_info_avatar").append($prev_img); - select_rm_info(`Character created

${DOMPurify.sanitize(save_name)}

`, oldSelectedChar); + select_rm_info(`char_create`, save_name, oldSelectedChar); $("#rm_info_block").transition({ opacity: 1.0, duration: 2000 }); crop_data = undefined; @@ -5415,8 +5464,8 @@ $(document).ready(function () { error: function (jqXHR, exception) { $("#create_button").removeAttr("disabled"); $("#result_info").html("Error: no connection"); - console.log('Cant use that name! Invalid or already exists.'); - alert('Cant use that name! Invalid or already exists.'); + console.log('Error! Either a file with the same name already existed, or the image file provided was in an invalid format. Double check that the image is not a webp.'); + alert('Error! Either a file with the same name already existed, or the image file provided was in an invalid format. Double check that the image is not a webp.'); }, }); } @@ -5424,8 +5473,11 @@ $(document).ready(function () { $("#delete_button").click(function () { popup_type = "del_ch"; - callPopup( - "

Delete the character?

Your chat will be closed." + callPopup(` +

Delete the character?

+ THIS IS PERMANENT!

+ THIS WILL ALSO DELETE ALL
+ OF THE CHARACTER'S CHAT FILES.

` ); }); @@ -5543,7 +5595,7 @@ $(document).ready(function () { let value = formatKoboldUrl($.trim($("#api_url_text").val())); if (!value) { - callPopup('Please enter a valid URL.', 'text'); + toastr.error('Please enter a valid URL.'); return; } @@ -5627,22 +5679,35 @@ $(document).ready(function () { var id = $(this).attr("id"); if (id == "option_toggle_AN") { - if (selected_group || this_chid !== undefined && $("#floatingPrompt").css("display") === 'none') { - $("#floatingPrompt").css("display", "flex"); - $("#floatingPrompt").css("opacity", 0.0); - $("#floatingPrompt").transition({ - opacity: 1.0, - duration: animation_duration, - easing: animation_easing, - }); - $("#ANBlockToggle").click(); - } else { - $("#floatingPrompt").transition({ - opacity: 0.0, - duration: 250, - easing: animation_easing, - }); - setTimeout(function () { $("#floatingPrompt").hide(); }, 250); + if (selected_group || this_chid) { + if ($("#floatingPrompt").css("display") !== 'flex') { + $("#floatingPrompt").css("display", "flex"); + $("#floatingPrompt").css("opacity", 0.0); + $("#floatingPrompt").transition({ + opacity: 1.0, + duration: 250, + easing: animation_easing, + }); + console.log('displayed AN panel'); + + if ($("#ANBlockToggle") + .siblings('.inline-drawer-content') + .css('display') !== 'block') { + $("#ANBlockToggle").click(); + console.log('opened AN box'); + } + } else { + $("#floatingPrompt").transition({ + opacity: 0.0, + duration: 250, + easing: animation_easing, + }); + setTimeout(function () { + $("#floatingPrompt").hide(); + console.log('hid AN panel'); + }, 250); + + } } } @@ -5931,11 +5996,12 @@ $(document).ready(function () { //***Message Editor*** $(document).on("click", ".mes_edit", function () { if (this_chid !== undefined || selected_group) { - const message = $(this).closest(".mes"); + // Previously system messages we're allowed to be edited + /*const message = $(this).closest(".mes"); if (message.data("isSystem")) { return; - } + }*/ let chatScrollPosition = $("#chat").scrollTop(); if (this_edit_mes_id !== undefined) { @@ -6165,8 +6231,6 @@ $(document).ready(function () { return; } - let names = []; - for (const file of e.target.files) { var ext = file.name.match(/\.(\w+)$/); if ( @@ -6203,13 +6267,11 @@ $(document).ready(function () { let oldSelectedChar = null; if (this_chid != undefined && this_chid != "invalid-safety-id") { - oldSelectedChar = characters[this_chid].name; + oldSelectedChar = characters[this_chid].avatar; } - names.push(data.file_name); - let nameString = DOMPurify.sanitize(names.join(', ')); await getCharacters(); - select_rm_info(`Character imported

${nameString}

`, oldSelectedChar); + select_rm_info(`char_import`, data.file_name, oldSelectedChar); $("#rm_info_block").transition({ opacity: 1, duration: 1000 }); } }, @@ -6257,12 +6319,13 @@ $(document).ready(function () { $("#chat_import_file").click(); }); - $("#chat_import_file").on("change", function (e) { + $("#chat_import_file").on("change", async function (e) { var file = e.target.files[0]; - //console.log(1); + if (!file) { return; } + var ext = file.name.match(/\.(\w+)$/); if ( !ext || @@ -6271,33 +6334,23 @@ $(document).ready(function () { return; } + if (selected_group && file.name.endsWith('.json')) { + toastr.warning("Only SillyTavern's own format is supported for group chat imports. Sorry!"); + return; + } + var format = ext[1].toLowerCase(); $("#chat_import_file_type").val(format); - //console.log(format); + var formData = new FormData($("#form_import_chat").get(0)); - //console.log('/importchat entered with: '+formData); - jQuery.ajax({ - type: "POST", - url: "/importchat", - data: formData, - beforeSend: function () { - $("#select_chat_div").html(""); - $("#load_select_chat_div").css("display", "block"); - //$('#create_button').attr('value','Creating...'); - }, - cache: false, - contentType: false, - processData: false, - success: function (data) { - //console.log(data); - if (data.res) { - displayPastChats(); - } - }, - error: function (jqXHR, exception) { - $("#create_button").removeAttr("disabled"); - }, - }); + $("#select_chat_div").html(""); + $("#load_select_chat_div").css("display", "block"); + + if (selected_group) { + await importGroupChat(formData); + } else { + await importCharacterChat(formData); + } }); $("#rm_button_group_chats").click(function () { diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js index cdd2cf607..962845331 100644 --- a/public/scripts/RossAscends-mods.js +++ b/public/scripts/RossAscends-mods.js @@ -423,22 +423,25 @@ function isUrlOrAPIKey(string) { } function OpenNavPanels() { - //auto-open R nav if locked and previously open - if (LoadLocalBool("NavLockOn") == true && LoadLocalBool("NavOpened") == true) { - //console.log("RA -- clicking right nav to open"); - $("#rightNavDrawerIcon").click(); - } - //auto-open L nav if locked and previously open - if (LoadLocalBool("LNavLockOn") == true && LoadLocalBool("LNavOpened") == true) { - console.log("RA -- clicking left nav to open"); - $("#leftNavDrawerIcon").click(); - } + if (deviceInfo.device.type === 'desktop') { + //auto-open R nav if locked and previously open + if (LoadLocalBool("NavLockOn") == true && LoadLocalBool("NavOpened") == true) { + //console.log("RA -- clicking right nav to open"); + $("#rightNavDrawerIcon").click(); + } - //auto-open WI if locked and previously open - if (LoadLocalBool("WINavLockOn") == true && LoadLocalBool("WINavOpened") == true) { - console.log("RA -- clicking WI to open"); - $("#WIDrawerIcon").click(); + //auto-open L nav if locked and previously open + if (LoadLocalBool("LNavLockOn") == true && LoadLocalBool("LNavOpened") == true) { + console.log("RA -- clicking left nav to open"); + $("#leftNavDrawerIcon").click(); + } + + //auto-open WI if locked and previously open + if (LoadLocalBool("WINavLockOn") == true && LoadLocalBool("WINavOpened") == true) { + console.log("RA -- clicking WI to open"); + $("#WIDrawerIcon").click(); + } } } diff --git a/public/scripts/bookmarks.js b/public/scripts/bookmarks.js index df8c41014..cf39397c6 100644 --- a/public/scripts/bookmarks.js +++ b/public/scripts/bookmarks.js @@ -266,7 +266,7 @@ async function convertSoloToGroupChat() { $(`.group_select[grid="${group.id}"]`).click(); await delay(1); - callPopup('The chat has been successfully converted!', 'text'); + toastr.success('The chat has been successfully converted!'); } $(document).ready(function () { diff --git a/public/scripts/context-template.js b/public/scripts/context-template.js new file mode 100644 index 000000000..d7480b1dc --- /dev/null +++ b/public/scripts/context-template.js @@ -0,0 +1,28 @@ +import { + callPopup, +} from '../script.js'; + +function openContextTemplateEditor() { + const editor = $('#context_editor_template .context_editor').clone(); + $('#dialogue_popup').addClass('large_dialogue_popup wide_dialogue_popup'); + callPopup(editor.html(), 'text'); +} + +function copyTemplateParameter(event) { + const text = $(event.target).text(); + navigator.clipboard.writeText(text); + const copiedMsg = document.createElement("div"); + copiedMsg.classList.add('code-copied'); + copiedMsg.innerText = "Copied!"; + copiedMsg.style.top = `${event.clientY - 55}px`; + copiedMsg.style.left = `${event.clientX - 55}px`; + document.body.append(copiedMsg); + setTimeout(() => { + document.body.removeChild(copiedMsg); + }, 1000); +} + +jQuery(() => { + $('#context_template_edit').on('click', openContextTemplateEditor); + $(document).on('pointerup', '.template_parameters_list code', copyTemplateParameter); +}) \ No newline at end of file diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index 4ffd249a6..2ce4d4b4c 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -195,7 +195,6 @@ async function connectToApi(baseUrl) { modules = data.modules; await activateExtensions(); eventSource.emit(event_types.EXTRAS_CONNECTED, modules); - $("#extensionsMenuButton").css("display", "flex"); } updateStatus(getExtensionsResult.ok); @@ -319,12 +318,12 @@ async function loadExtensionSettings(settings) { } } -async function runGenerationInterceptors() { +async function runGenerationInterceptors(chat) { for (const manifest of Object.values(manifests)) { const interceptorKey = manifest.generate_interceptor; if (typeof window[interceptorKey] === 'function') { try { - await window[interceptorKey](); + await window[interceptorKey](chat); } catch(e) { console.error(`Failed running interceptor for ${manifest.display_name}`, e); } @@ -333,7 +332,11 @@ async function runGenerationInterceptors() { } $(document).ready(async function () { - setTimeout(function () { addExtensionsButtonAndMenu(); }, 100) + setTimeout(function () { + addExtensionsButtonAndMenu(); + $("#extensionsMenuButton").css("display", "flex"); + }, 100) + $("#extensions_connect").on('click', connectClickHandler); $("#extensions_autoconnect").on('input', autoConnectInputHandler); $("#extensions_details").on('click', showExtensionsDetails); diff --git a/public/scripts/extensions/caption/index.js b/public/scripts/extensions/caption/index.js index 992413213..535be7620 100644 --- a/public/scripts/extensions/caption/index.js +++ b/public/scripts/extensions/caption/index.js @@ -15,9 +15,9 @@ async function moduleWorker() { async function setImageIcon() { try { - const sendButton = document.getElementById('send_picture'); - sendButton.classList.add('fa-image'); - sendButton.classList.remove('fa-hourglass-half'); + const sendButton = $('#send_picture .extensionsMenuExtensionButton'); + sendButton.addClass('fa-image'); + sendButton.removeClass('fa-hourglass-half'); } catch (error) { console.log(error); @@ -26,9 +26,9 @@ async function setImageIcon() { async function setSpinnerIcon() { try { - const sendButton = document.getElementById('send_picture'); - sendButton.classList.remove('fa-image'); - sendButton.classList.add('fa-hourglass-half'); + const sendButton = $('#send_picture .extensionsMenuExtensionButton'); + sendButton.removeClass('fa-image'); + sendButton.addClass('fa-hourglass-half'); } catch (error) { console.log(error); @@ -92,15 +92,17 @@ async function onSelectImage(e) { } } -$(document).ready(function () { +jQuery(function () { function addSendPictureButton() { - const sendButton = document.createElement('div'); - sendButton.id = 'send_picture'; - sendButton.title = 'Send a picture to chat'; - sendButton.classList.add('fa-solid'); + const sendButton = $(` +
+
+ Send a picture +
`); + + $('#extensionsMenu').prepend(sendButton); $(sendButton).hide(); - $(sendButton).on('click', () => $('#img_file').click()); - $('#send_but_sheld').prepend(sendButton); + $(sendButton).on('click', () => $('#img_file').trigger('click')); } function addPictureSendForm() { const inputHtml = ``; diff --git a/public/scripts/extensions/caption/style.css b/public/scripts/extensions/caption/style.css index 044c21800..58f93eb55 100644 --- a/public/scripts/extensions/caption/style.css +++ b/public/scripts/extensions/caption/style.css @@ -1,24 +1,3 @@ -#send_picture { - order: 200; - 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; -} - -#send_picture:hover { - opacity: 1; - filter: brightness(1.2); -} - #img_form { display: none; } diff --git a/public/scripts/extensions/floating-prompt/index.js b/public/scripts/extensions/floating-prompt/index.js index c5a935e92..f1763732b 100644 --- a/public/scripts/extensions/floating-prompt/index.js +++ b/public/scripts/extensions/floating-prompt/index.js @@ -20,8 +20,50 @@ const metadata_keys = { position: 'note_position', } -function setNoteCommand(_, text) { +function setNoteTextCommand(_, text) { $('#extension_floating_prompt').val(text).trigger('input'); + toastr.success("Author's Note text updated"); +} + +function setNoteDepthCommand(_, text) { + const value = Number(text); + + if (Number.isNaN(value)) { + toastr.error('Not a valid number'); + return; + } + + $('#extension_floating_depth').val(Math.abs(value)).trigger('input'); + toastr.success("Author's Note depth updated"); +} + +function setNoteIntervalCommand(_, text) { + const value = Number(text); + + if (Number.isNaN(value)) { + toastr.error('Not a valid number'); + return; + } + + $('#extension_floating_interval').val(Math.abs(value)).trigger('input'); + toastr.success("Author's Note frequency updated"); +} + +function setNotePositionCommand(_, text) { + const validPositions = { + 'scenario': 0, + 'chat': 1, + }; + + const position = validPositions[text?.trim()]; + + if (Number.isNaN(position)) { + toastr.error('Not a valid position'); + return; + } + + $(`input[name="extension_floating_position"][value="${position}"]`).prop('checked', true).trigger('input'); + toastr.info("Author's Note position updated"); } async function onExtensionFloatingPromptInput() { @@ -185,5 +227,8 @@ async function moduleWorker() { addExtensionsSettings(); setInterval(moduleWorkerWrapper, UPDATE_INTERVAL); - registerSlashCommand('note', setNoteCommand, [], " – sets an author's note for the currently selected chat", true, true); + registerSlashCommand('note', setNoteTextCommand, [], "(text) – sets an author's note for the currently selected chat", true, true); + registerSlashCommand('depth', setNoteDepthCommand, [], "(number) – sets an author's note depth for in-chat positioning", true, true); + registerSlashCommand('freq', setNoteIntervalCommand, ['interval'], "(number) – sets an author's note insertion frequency", true, true); + registerSlashCommand('pos', setNotePositionCommand, ['position'], "(chat or scenario) – sets an author's note position", true, true); })(); \ No newline at end of file diff --git a/public/scripts/extensions/floating-prompt/manifest.json b/public/scripts/extensions/floating-prompt/manifest.json index 64fac1b56..edcacbc74 100644 --- a/public/scripts/extensions/floating-prompt/manifest.json +++ b/public/scripts/extensions/floating-prompt/manifest.json @@ -1,5 +1,5 @@ { - "display_name": "Author's Note / Character Bias", + "display_name": "Author's Note (Located in Lower Left Options Menu)", "loading_order": 1, "requires": [], "optional": [], diff --git a/public/scripts/extensions/stable-diffusion/index.js b/public/scripts/extensions/stable-diffusion/index.js index 86356ec0c..1cadb1292 100644 --- a/public/scripts/extensions/stable-diffusion/index.js +++ b/public/scripts/extensions/stable-diffusion/index.js @@ -56,7 +56,31 @@ const quietPrompts = { //prompt for only the last message [generationMode.USER]: "[Pause your roleplay and provide a detailed description of {{user}}'s physical appearance from the perspective of {{char}} in the form of a comma-delimited list of keywords and phrases. 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,'. Ignore the rest of the story when crafting this description. Do not roleplay as {{char}} when writing this description, and do not attempt to continue the story.]", [generationMode.SCENARIO]: "[Pause your roleplay and provide a detailed description for all of the following: a brief recap of recent events in the story, {{char}}'s appearance, and {{char}}'s surroundings. Do not roleplay while writing this description.]", - [generationMode.NOW]: "[Pause your roleplay and provide a brief description of the last chat message. Focus on visual details, clothing, actions and make them all coherent to the current state of the story. Ignore the non-visible feelings and thoughts of {{char}} and {{user}} as well as any spoken dialog. Do not roleplay as {{char}} while writing this description. Do not continue the roleplay story.]", + + [generationMode.NOW]: `[Pause your roleplay. Your next response must be formatted as a single comma-delimited list of concise keywords. The list will describe of the visual details included in the last chat message. + + Only mention characters by using pronouns ('he','his','she','her','it','its') or neutral nouns ('male', 'the man', 'female', 'the woman'). + + Ignore non-visible things such as feelings, personality traits, thoughts, and spoken dialog. + + Add keywords in this precise order: + a keyword to describe the location of the scene, + a keyword to mention how many characters of each gender or type are present in the scene (minimum of two characters: + {{user}} and {{char}}, example: '2 men ' or '1 man 1 woman ', '1 man 3 robots'), + + keywords to describe the relative physical positioning of the characters to each other (if a commonly known term for the positioning is known use it instead of describing the positioning in detail) + 'POV', + + a single keyword or phrase to describe the primary act taking place in the last chat message, + + keywords to describe {{char}}'s physical appearance and facial expression, + keywords to describe {{char}}'s actions, + keywords to describe {{user}}'s physical appearance and actions. + + If character actions involve direct physical interaction with another character, mention specifically which body parts interacting and how. + + A correctly formatted example response would be: + '(location),(character list by gender),(primary action), (relative character position) POV, (character 1's description and actions), (character 2's description and actions)']`, + [generationMode.RAW_LAST]: "[Pause your roleplay and provide ONLY the last chat message string back to me verbatim. Do not write anything after the string. Do not roleplay at all in your response. Do not continue the roleplay story.]", } @@ -94,7 +118,7 @@ const defaultSettings = { width: 512, height: 512, - prompt_prefix: 'best quality, absurdres, masterpiece, detailed, intricate,', + 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', sampler: 'DDIM', model: '', @@ -106,6 +130,7 @@ const defaultSettings = { // Horde settings horde: false, horde_nsfw: false, + horde_karras: true, } async function loadSettings() { @@ -121,6 +146,7 @@ async function loadSettings() { $('#sd_height').val(extension_settings.sd.height).trigger('input'); $('#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_restore_faces').prop('checked', extension_settings.sd.restore_faces); $('#sd_enable_hr').prop('checked', extension_settings.sd.enable_hr); @@ -181,6 +207,11 @@ async function onHordeNsfwInput() { saveSettingsDebounced(); } +async function onHordeKarrasInput() { + extension_settings.sd.horde_karras = !!$(this).prop('checked'); + saveSettingsDebounced(); +} + function onRestoreFacesInput() { extension_settings.sd.restore_faces = !!$(this).prop('checked'); saveSettingsDebounced(); @@ -289,8 +320,10 @@ async function loadHordeModels() { headers: getRequestHeaders(), }); + if (result.ok) { const data = await result.json(); + data.sort((a, b) => b.count - a.count); const models = data.map(x => ({ value: x.name, text: `${x.name} (ETA: ${x.eta}s, Queue: ${x.queued}, Workers: ${x.count})` })); return models; } @@ -351,6 +384,7 @@ function processReply(str) { str = str.replaceAll('"', '') str = str.replaceAll('“', '') + str = str.replaceAll('.', ',') str = str.replaceAll('\n', ', ') str = str.replace(/[^a-zA-Z0-9,:]+/g, ' ') // Replace everything except alphanumeric characters and commas with spaces str = str.replace(/\s+/g, ' '); // Collapse multiple whitespaces into one @@ -380,7 +414,7 @@ async function generatePicture(_, trigger, message, callback) { } if (!modules.includes('sd') && !extension_settings.sd.horde) { - callPopup("Extensions API is not connected or doesn't provide SD module. Enable Stable Horde to generate images.", 'text'); + toastr.warning("Extensions API is not connected or doesn't provide SD module. Enable Stable Horde to generate images."); return; } @@ -473,6 +507,7 @@ async function generateExtrasImage(prompt, callback) { negative_prompt: extension_settings.sd.negative_prompt, restore_faces: !!extension_settings.sd.restore_faces, enable_hr: !!extension_settings.sd.enable_hr, + karras: !!extension_settings.sd.horde_karras, }), }); @@ -510,7 +545,7 @@ async function generateHordeImage(prompt, callback) { const base64Image = `data:image/webp;base64,${data}`; callback ? callback(prompt, base64Image) : sendMessage(prompt, base64Image); } else { - callPopup('Image generation has failed. Please try again.', 'text'); + toastr.error('Image generation has failed. Please try again.'); } } @@ -537,9 +572,9 @@ async function sendMessage(prompt, image) { function addSDGenButtons() { const buttonHtml = ` -
+
- StableDiffusion + Stable Diffusion
`; @@ -592,40 +627,62 @@ function addSDGenButtons() { }); } -async function moduleWorker() { - const context = getContext(); +function isConnectedToExtras() { + return modules.includes('sd'); +} - if (context.onlineStatus === 'no_connection') { - $('#sd_gen').hide(200); - $('.sd_message_gen').hide(); - } - else { +async function moduleWorker() { + if (isConnectedToExtras() || extension_settings.sd.horde) { $('#sd_gen').show(200); $('.sd_message_gen').show(); } + else { + $('#sd_gen').hide(200); + $('.sd_message_gen').hide(); + } } addSDGenButtons(); - setInterval(moduleWorker, UPDATE_INTERVAL); -function sdMessageButton(e) { +async function sdMessageButton(e) { + function setBusyIcon(isBusy) { + $icon.toggleClass('fa-paintbrush', !isBusy); + $icon.toggleClass(busyClass, isBusy); + } + + const busyClass = 'fa-hourglass'; const context = getContext(); - const $mes = $(e.currentTarget).closest('.mes'); + const $icon = $(e.currentTarget); + const $mes = $icon.closest('.mes'); const characterName = $mes.find('.name_text').text(); const messageText = $mes.find('.mes_text').text(); const message_id = $mes.attr('mesid'); const message = context.chat[message_id]; const hasSavedImage = message?.extra?.image && message?.extra?.title; - if (hasSavedImage) { - const prompt = message?.extra?.title; - console.log('Regenerating an image, using existing prompt:', prompt); - sendGenerationRequest(prompt, saveGeneratedImage); + if ($icon.hasClass(busyClass)) { + console.log('Previous image is still being generated...'); + return; } - else { - console.log("doing /sd raw last"); - generatePicture('sd', 'raw_last', `${characterName} said: ${messageText}`, saveGeneratedImage); + + try { + setBusyIcon(true); + if (hasSavedImage) { + const prompt = message?.extra?.title; + console.log('Regenerating an image, using existing prompt:', prompt); + await sendGenerationRequest(prompt, saveGeneratedImage); + } + else { + console.log("doing /sd raw last"); + await generatePicture('sd', 'raw_last', `${characterName} said: ${messageText}`, saveGeneratedImage); + } + } + catch (error) { + console.error('Could not generate inline image: ', error); + } + finally { + setBusyIcon(false); } function saveGeneratedImage(prompt, image) { @@ -689,7 +746,7 @@ jQuery(async () => {
- Use slash commands or the bottom Paintbrush buttron to generate images. Type /help in chat for more details + Use slash commands or the bottom Paintbrush button to generate images. Type /help in chat for more details
Hint: Save an API key in Horde KoboldAI API settings to use it here.
@@ -725,6 +782,12 @@ jQuery(async () => { +
+ +
@@ -743,6 +806,7 @@ jQuery(async () => { $('#sd_height').on('input', onHeightInput); $('#sd_horde').on('input', onHordeInput); $('#sd_horde_nsfw').on('input', onHordeNsfwInput); + $('#sd_horde_karras').on('input', onHordeKarrasInput); $('#sd_restore_faces').on('input', onRestoreFacesInput); $('#sd_enable_hr').on('input', onHighResFixInput); diff --git a/public/scripts/extensions/tts/elevenlabs.js b/public/scripts/extensions/tts/elevenlabs.js index 65c3cf302..c039156d5 100644 --- a/public/scripts/extensions/tts/elevenlabs.js +++ b/public/scripts/extensions/tts/elevenlabs.js @@ -17,6 +17,7 @@ class ElevenLabsTtsProvider { stability: 0.75, similarity_boost: 0.75, apiKey: "", + multilingual: false, voiceMap: {} } @@ -28,6 +29,10 @@ class ElevenLabsTtsProvider { + ` return html } @@ -36,6 +41,7 @@ class ElevenLabsTtsProvider { // Update dynamically this.settings.stability = $('#elevenlabs_tts_stability').val() this.settings.similarity_boost = $('#elevenlabs_tts_similarity_boost').val() + this.settings.multilingual = $('#elevenlabs_tts_multilingual').prop('checked') } @@ -59,6 +65,7 @@ class ElevenLabsTtsProvider { $('#elevenlabs_tts_stability').val(this.settings.stability) $('#elevenlabs_tts_similarity_boost').val(this.settings.similarity_boost) $('#elevenlabs_tts_api_key').val(this.settings.apiKey) + $('#tts_auto_generation').prop('checked', this.settings.multilingual) console.info("Settings loaded") } @@ -165,6 +172,10 @@ class ElevenLabsTtsProvider { } async fetchTtsGeneration(text, voiceId) { + let model = "eleven_monolingual_v1" + if (this.settings.multilingual == true) { + model = "eleven_multilingual_v1" + } console.info(`Generating new TTS for voice_id ${voiceId}`) const response = await fetch( `https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`, @@ -175,6 +186,7 @@ class ElevenLabsTtsProvider { 'Content-Type': 'application/json' }, body: JSON.stringify({ + model: model, text: text, voice_settings: this.settings }) diff --git a/public/scripts/extensions/tts/index.js b/public/scripts/extensions/tts/index.js index 48d2d6d82..d7664e4d2 100644 --- a/public/scripts/extensions/tts/index.js +++ b/public/scripts/extensions/tts/index.js @@ -71,7 +71,6 @@ async function moduleWorker() { processAudioJobQueue() updateUiAudioPlayState() - // Auto generation is disabled if (extension_settings.tts.auto_generation == false) { return @@ -163,6 +162,23 @@ function isTtsProcessing() { return processing } +function debugTtsPlayback() { + console.log(JSON.stringify( + { + "ttsProviderName": ttsProviderName, + "currentMessageNumber": currentMessageNumber, + "isWorkerBusy":isWorkerBusy, + "audioPaused": audioPaused, + "audioJobQueue": audioJobQueue, + "currentAudioJob": currentAudioJob, + "audioQueueProcessorReady": audioQueueProcessorReady, + "ttsJobQueue": ttsJobQueue, + "currentTtsJob": currentTtsJob, + } + )) +} +window.debugTtsPlayback = debugTtsPlayback + //##################// // Audio Control // //##################// @@ -229,7 +245,7 @@ async function onTtsVoicesClick() { function updateUiAudioPlayState() { if (extension_settings.tts.enabled == true) { - audioControl.style.display = 'flex' + $('#ttsExtensionMenuItem').show(); let img // Give user feedback that TTS is active by setting the stop icon if processing or playing if (!audioElement.paused || isTtsProcessing()) { @@ -237,9 +253,9 @@ function updateUiAudioPlayState() { } else { img = 'fa-solid fa-circle-play extensionsMenuExtensionButton' } - audioControl.className = img + $('#tts_media_control').attr('class', img); } else { - audioControl.style.display = 'none' + $('#ttsExtensionMenuItem').hide(); } } @@ -262,7 +278,7 @@ function addAudioControl() {
TTS Playback
`) - $('#tts_media_control').attr('title', 'TTS play/pause').on('click', onAudioControlClicked) + $('#ttsExtensionMenuItem').attr('title', 'TTS play/pause').on('click', onAudioControlClicked) audioControl = document.getElementById('tts_media_control') updateUiAudioPlayState() } diff --git a/public/scripts/extensions/tts/system.js b/public/scripts/extensions/tts/system.js index dd8c324fb..799129051 100644 --- a/public/scripts/extensions/tts/system.js +++ b/public/scripts/extensions/tts/system.js @@ -30,7 +30,7 @@ class SystemTtsProvider { } get settingsHtml() { - if (!window.speechSynthesis) { + if (!('speechSynthesis' in window)) { return "Your browser or operating system doesn't support speech synthesis"; } @@ -81,7 +81,7 @@ class SystemTtsProvider { // TTS Interfaces // //#################// fetchTtsVoiceIds() { - if (!window.speechSynthesis) { + if (!('speechSynthesis' in window)) { return []; } @@ -92,6 +92,10 @@ class SystemTtsProvider { } previewTtsVoice(voiceId) { + if (!('speechSynthesis' in window)) { + throw 'Speech synthesis API is not supported'; + } + const voice = speechSynthesis.getVoices().find(x => x.voiceURI === voiceId); if (!voice) { @@ -108,11 +112,11 @@ class SystemTtsProvider { } async getVoice(voiceName) { - if (!window.speechSynthesis) { + if (!('speechSynthesis' in window)) { return { voice_id: null } } - const voices = window.speechSynthesis.getVoices(); + const voices = speechSynthesis.getVoices(); const match = voices.find(x => x.name == voiceName); if (!match) { @@ -123,7 +127,7 @@ class SystemTtsProvider { } async generateTts(text, voiceId) { - if (!window.speechSynthesis) { + if (!('speechSynthesis' in window)) { throw 'Speech synthesis API is not supported'; } diff --git a/public/scripts/group-chats.js b/public/scripts/group-chats.js index 171908216..90f189f1f 100644 --- a/public/scripts/group-chats.js +++ b/public/scripts/group-chats.js @@ -47,6 +47,7 @@ import { select_selected_character, cancelTtsPlay, isMultigenEnabled, + displayPastChats, } from "../script.js"; import { appendTagToList, createTagMapFromList, getTagsList, applyTagsOnCharacterSelect } from './tags.js'; @@ -292,6 +293,12 @@ async function getGroups() { if (group.past_metadata == undefined) { group.past_metadata = {}; } + if (typeof group.chat_id === 'number') { + group.chat_id = String(group.chat_id); + } + if (Array.isArray(group.chats) && group.chats.some(x => typeof x === 'number')) { + group.chats = group.chats.map(x => String(x)); + } } } } @@ -473,7 +480,7 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) { activatedMembers = activateSwipe(group.members); if (activatedMembers.length === 0) { - callPopup('

Deleted group member swiped. To get a reply, add them back to the group.

', 'text'); + toastr.warning('Deleted group member swiped. To get a reply, add them back to the group.'); throw new Error('Deleted group member swiped'); } } @@ -954,7 +961,7 @@ function select_group_chats(groupId, skipAnimation) { $("#rm_group_delete").off(); $("#rm_group_delete").on("click", function () { if (is_group_generating) { - callPopup('

Not so fast! Wait for the characters to stop typing before deleting the group.

', 'text'); + toastr.warning('Not so fast! Wait for the characters to stop typing before deleting the group.'); return; } @@ -1282,6 +1289,34 @@ export async function deleteGroupChat(groupId, chatId) { } } +export async function importGroupChat(formData) { + await jQuery.ajax({ + type: "POST", + url: "/importgroupchat", + data: formData, + beforeSend: function () { + }, + cache: false, + contentType: false, + processData: false, + success: async function (data) { + if (data.res) { + const chatId = data.res; + const group = groups.find(x => x.id == selected_group); + + if (group) { + group.chats.push(chatId); + await editGroup(selected_group, true, true); + await displayPastChats(); + } + } + }, + error: function () { + $("#create_button").removeAttr("disabled"); + }, + }); +} + export async function saveGroupBookmarkChat(groupId, name, metadata) { const group = groups.find(x => x.id === groupId); diff --git a/public/scripts/horde.js b/public/scripts/horde.js index 13dfa71fb..77ee40908 100644 --- a/public/scripts/horde.js +++ b/public/scripts/horde.js @@ -41,7 +41,7 @@ function validateHordeModel() { let selectedModels = models.filter(m => horde_settings.models.includes(m.name)); if (selectedModels.length === 0) { - callPopup('No Horde model selected or the selected models are no longer available. Please choose another model', 'text'); + toastr.warning('No Horde model selected or the selected models are no longer available. Please choose another model'); throw new Error('No Horde model available'); } @@ -102,7 +102,7 @@ async function generateHorde(prompt, params) { const response = await fetch("/generate_horde", { method: 'POST', headers: { - ...getRequestHeaders(), + ...getRequestHeaders(), "Client-Agent": CLIENT_VERSION, }, body: JSON.stringify(payload) @@ -129,9 +129,10 @@ async function generateHorde(prompt, params) { setGenerationProgress(100); const generatedText = statusCheckJson.generations[0].text; const WorkerName = statusCheckJson.generations[0].worker_name; + const WorkerModel = statusCheckJson.generations[0].model; console.log(generatedText); - console.log(`Generated by Horde Worker: ${WorkerName}`); - return { text: generatedText, workerName: `Generated by Horde worker: ${WorkerName}` }; + console.log(`Generated by Horde Worker: ${WorkerName} [${WorkerModel}]`); + return { text: generatedText, workerName: `Generated by Horde worker: ${WorkerName} [${WorkerModel}]` }; } else if (!queue_position_first) { queue_position_first = statusCheckJson.queue_position; @@ -184,6 +185,28 @@ function loadHordeSettings(settings) { $('#horde_auto_adjust_context_length').prop("checked", horde_settings.auto_adjust_context_length); } +async function showKudos() { + const response = await fetch('/horde_userinfo', { + method: 'POST', + headers: getRequestHeaders(), + }); + + if (!response.ok) { + toastr.warning('Could not load user info from Horde. Please try again later.'); + return; + } + + const data = await response.json(); + + if (data.anonymous) { + toastr.info('You are in anonymous mode. Set your personal Horde API key to see kudos.') + return; + } + + console.log('Horde user data', data); + toastr.info(`${data.username}
Kudos: ${data.kudos}`); +} + jQuery(function () { $("#use_horde").on("input", async function () { horde_settings.use_horde = !!$(this).prop("checked"); @@ -224,4 +247,5 @@ jQuery(function () { }); $("#horde_refresh").on("click", getHordeModels); -}) \ No newline at end of file + $("#horde_kudos").on("click", showKudos); +}) diff --git a/public/scripts/openai.js b/public/scripts/openai.js index b94b9074c..871b14bd2 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -18,6 +18,7 @@ import { callPopup, getRequestHeaders, system_message_types, + replaceBiasMarkup, } from "../script.js"; import { groups, selected_group } from "./group-chats.js"; @@ -102,7 +103,7 @@ const default_settings = { openai_model: 'gpt-3.5-turbo', jailbreak_system: false, reverse_proxy: '', - oai_breakdown: false, + legacy_streaming: false, }; const oai_settings = { @@ -127,7 +128,7 @@ const oai_settings = { openai_model: 'gpt-3.5-turbo', jailbreak_system: false, reverse_proxy: '', - oai_breakdown: false, + legacy_streaming: false, }; let openai_setting_names; @@ -147,7 +148,7 @@ function validateReverseProxy() { new URL(oai_settings.reverse_proxy); } catch (err) { - callPopup('Entered reverse proxy address is not a valid URL', 'text'); + toastr.error('Entered reverse proxy address is not a valid URL'); setOnlineStatus('no_connection'); resultCheckStatusOpen(); throw err; @@ -171,13 +172,12 @@ function setOpenAIMessages(chat) { role = 'system'; } - // for groups - prepend a character's name - if (selected_group) { + // for groups or sendas command - prepend a character's name + if (selected_group || chat[j].force_avatar) { content = `${chat[j].name}: ${content}`; } - // replace bias markup - content = (content ?? '').replace(/{{(\*?.*\*?)}}/g, ''); + content = replaceBiasMarkup(content); // remove caret return (waste of tokens) content = content.replace(/\r/gm, ''); @@ -460,7 +460,7 @@ async function prepareOpenAIMessages(name2, storyString, worldInfoBefore, worldI handler_instance.log(); return [ openai_msgs_tosend, - oai_settings.oai_breakdown ? handler_instance.counts : false, + handler_instance.counts, ]; } @@ -562,13 +562,25 @@ async function sendOpenAIRequest(type, openai_msgs_tosend, signal) { const decoder = new TextDecoder(); const reader = response.body.getReader(); let getMessage = ""; + let messageBuffer = ""; while (true) { const { done, value } = await reader.read(); let response = decoder.decode(value); tryParseStreamingError(response); - let eventList = response.split("\n"); + let eventList = []; + + // ReadableStream's buffer is not guaranteed to contain full SSE messages as they arrive in chunks + // We need to buffer chunks until we have one or more full messages (separated by double newlines) + if (!oai_settings.legacy_streaming) { + messageBuffer += response; + eventList = messageBuffer.split("\n\n"); + // Last element will be an empty string or a leftover partial message + messageBuffer = eventList.pop(); + } else { + eventList = response.split("\n"); + } for (let event of eventList) { if (!event.startsWith("data")) @@ -690,7 +702,7 @@ function countTokens(messages, full = false) { else { jQuery.ajax({ async: false, - type: 'POST', // + type: 'POST', // url: `/tokenize_openai?model=${oai_settings.openai_model}`, data: JSON.stringify([message]), dataType: "json", @@ -737,6 +749,7 @@ function loadOpenAISettings(data, settings) { oai_settings.openai_max_tokens = settings.openai_max_tokens ?? default_settings.openai_max_tokens; oai_settings.bias_preset_selected = settings.bias_preset_selected ?? default_settings.bias_preset_selected; oai_settings.bias_presets = settings.bias_presets ?? default_settings.bias_presets; + oai_settings.legacy_streaming = settings.legacy_streaming ?? default_settings.legacy_streaming; if (settings.nsfw_toggle !== undefined) oai_settings.nsfw_toggle = !!settings.nsfw_toggle; if (settings.keep_example_dialogue !== undefined) oai_settings.keep_example_dialogue = !!settings.keep_example_dialogue; @@ -745,7 +758,6 @@ function loadOpenAISettings(data, settings) { if (settings.nsfw_first !== undefined) oai_settings.nsfw_first = !!settings.nsfw_first; if (settings.openai_model !== undefined) oai_settings.openai_model = settings.openai_model; if (settings.jailbreak_system !== undefined) oai_settings.jailbreak_system = !!settings.jailbreak_system; - if (settings.oai_breakdown !== undefined) oai_settings.oai_breakdown = !!settings.oai_breakdown; $('#stream_toggle').prop('checked', oai_settings.stream_openai); @@ -761,7 +773,7 @@ function loadOpenAISettings(data, settings) { $('#wrap_in_quotes').prop('checked', oai_settings.wrap_in_quotes); $('#nsfw_first').prop('checked', oai_settings.nsfw_first); $('#jailbreak_system').prop('checked', oai_settings.jailbreak_system); - $('#oai_breakdown').prop('checked', oai_settings.oai_breakdown); + $('#legacy_streaming').prop('checked', oai_settings.legacy_streaming); if (settings.main_prompt !== undefined) oai_settings.main_prompt = settings.main_prompt; if (settings.nsfw_prompt !== undefined) oai_settings.nsfw_prompt = settings.nsfw_prompt; @@ -810,8 +822,8 @@ async function getStatusOpen() { }; return jQuery.ajax({ - type: 'POST', // - url: '/getstatus_openai', // + type: 'POST', // + url: '/getstatus_openai', // data: JSON.stringify(data), beforeSend: function () { if (oai_settings.reverse_proxy) { @@ -881,7 +893,8 @@ async function saveOpenAIPreset(name, settings) { jailbreak_system: settings.jailbreak_system, impersonation_prompt: settings.impersonation_prompt, bias_preset_selected: settings.bias_preset_selected, - oai_breakdown: settings.oai_breakdown, + reverse_proxy: settings.reverse_proxy, + legacy_streaming: settings.legacy_streaming, }; const savePresetSettings = await fetch(`/savepreset_openai?name=${name}`, { @@ -928,7 +941,7 @@ async function showApiKeyUsage() { } catch (err) { console.error(err); - callPopup('Invalid API key', 'text'); + toastr.error('Invalid API key'); } } @@ -993,7 +1006,7 @@ async function createNewLogitBiasPreset() { } if (name in oai_settings.bias_presets) { - callPopup('Preset name should be unique.', 'text'); + toastr.error('Preset name should be unique.'); return; } @@ -1030,12 +1043,12 @@ async function onLogitBiasPresetImportFileChange(e) { e.target.value = ''; if (name in oai_settings.bias_presets) { - callPopup('Preset name should be unique.', 'text'); + toastr.error('Preset name should be unique.'); return; } if (!Array.isArray(importedFile)) { - callPopup('Invalid logit bias preset file.', 'text'); + toastr.error('Invalid logit bias preset file.'); return; } @@ -1140,12 +1153,13 @@ function onSettingsPresetChange() { wrap_in_quotes: ['#wrap_in_quotes', 'wrap_in_quotes', true], nsfw_first: ['#nsfw_first', 'nsfw_first', true], jailbreak_system: ['#jailbreak_system', 'jailbreak_system', true], - oai_breakdown: ['#oai_breakdown', 'oai_breakdown', true], main_prompt: ['#main_prompt_textarea', 'main_prompt', false], nsfw_prompt: ['#nsfw_prompt_textarea', 'nsfw_prompt', false], jailbreak_prompt: ['#jailbreak_prompt_textarea', 'jailbreak_prompt', false], impersonation_prompt: ['#impersonation_prompt_textarea', 'impersonation_prompt', false], bias_preset_selected: ['#openai_logit_bias_preset', 'bias_preset_selected', false], + reverse_proxy: ['#openai_reverse_proxy', 'reverse_proxy', false], + legacy_streaming: ['#legacy_streaming', 'legacy_streaming', false], }; for (const [key, [selector, setting, isCheckbox]] of Object.entries(settingsToUpdate)) { @@ -1313,16 +1327,6 @@ $(document).ready(function () { saveSettingsDebounced(); }); - $("#oai_breakdown").on('change', function () { - oai_settings.oai_breakdown = !!$(this).prop("checked"); - if (!oai_settings.oai_breakdown) { - $("#token_breakdown").css('display', 'none'); - } else { - $("#token_breakdown").css('display', 'flex'); - } - saveSettingsDebounced(); - }); - // auto-select a preset based on character/group name $(document).on("click", ".character_select", function () { const chid = $(this).attr('chid'); @@ -1349,7 +1353,7 @@ $(document).ready(function () { $("#update_oai_preset").on('click', async function () { const name = oai_settings.preset_settings_openai; await saveOpenAIPreset(name, oai_settings); - callPopup('Preset updated', 'text'); + toastr.success('Preset updated'); }); $("#main_prompt_restore").on('click', function () { @@ -1376,6 +1380,11 @@ $(document).ready(function () { saveSettingsDebounced(); }); + $('#legacy_streaming').on('input', function () { + oai_settings.legacy_streaming = !!$(this).prop('checked'); + saveSettingsDebounced(); + }); + $("#api_button_openai").on("click", onConnectButtonClick); $("#openai_reverse_proxy").on("input", onReverseProxyInput); $("#model_openai_select").on("change", onModelChange); diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index ba2fabb4c..b67bc1afd 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -134,6 +134,7 @@ let power_user = { input_sequence: '### Instruction:', output_sequence: '### Response:', preset: 'Alpaca', + separator_sequence: '', } }; @@ -584,6 +585,7 @@ function loadInstructMode() { { id: "instruct_wrap", property: "wrap", isCheckbox: true }, { id: "instruct_system_prompt", property: "system_prompt", isCheckbox: false }, { id: "instruct_system_sequence", property: "system_sequence", isCheckbox: false }, + { id: "instruct_separator_sequence", property: "separator_sequence", isCheckbox: false }, { id: "instruct_input_sequence", property: "input_sequence", isCheckbox: false }, { id: "instruct_output_sequence", property: "output_sequence", isCheckbox: false }, { id: "instruct_stop_sequence", property: "stop_sequence", isCheckbox: false }, @@ -638,11 +640,14 @@ function loadInstructMode() { }); } -export function formatInstructModeChat(name, mes, isUser, isNarrator) { - const includeNames = isNarrator ? false : power_user.instruct.names || !!selected_group; +export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvatar) { + const includeNames = isNarrator ? false : (power_user.instruct.names || !!selected_group || !!forceAvatar); const sequence = (isUser || isNarrator) ? power_user.instruct.input_sequence : power_user.instruct.output_sequence; - const separator = power_user.instruct.wrap ? '\n' : ''; - const textArray = includeNames ? [sequence, `${name}: ${mes}`, separator] : [sequence, mes, separator]; + const separator = power_user.instruct.wrap ? '\n' : ''; + const separatorSequence = power_user.instruct.separator_sequence && !isUser + ? power_user.instruct.separator_sequence + : (power_user.instruct.wrap ? '\n' : ''); + const textArray = includeNames ? [sequence, `${name}: ${mes}`, separatorSequence] : [sequence, mes, separatorSequence]; const text = textArray.filter(x => x).join(separator); return text; } @@ -656,12 +661,17 @@ export function formatInstructStoryString(story) { return text; } -export function formatInstructModePrompt(name, isImpersonate) { +export function formatInstructModePrompt(name, isImpersonate, promptBias) { const includeNames = power_user.instruct.names || !!selected_group; const sequence = isImpersonate ? power_user.instruct.input_sequence : power_user.instruct.output_sequence; const separator = power_user.instruct.wrap ? '\n' : ''; - const text = includeNames ? (separator + sequence + separator + `${name}:`) : (separator + sequence); - return text; + let text = includeNames ? (separator + sequence + separator + `${name}:`) : (separator + sequence); + + if (!isImpersonate && promptBias) { + text += (includeNames ? promptBias : (separator + promptBias)); + } + + return text.trimEnd(); } const sortFunc = (a, b) => power_user.sort_order == 'asc' ? compareFunc(a, b) : compareFunc(b, a); diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index b3168c92d..584bbb40c 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -1,7 +1,12 @@ import { addOneMessage, + characters, chat, chat_metadata, + default_avatar, + extractMessageBias, + getThumbnailUrl, + replaceBiasMarkup, saveChatConditional, sendSystemMessage, system_avatar, @@ -22,6 +27,11 @@ class SlashCommandParser { addCommand(command, callback, aliases, helpString = '', interruptsGeneration = false, purgeFromMessage = true) { const fnObj = { callback, helpString, interruptsGeneration, purgeFromMessage }; + + if ([command, ...aliases].some(x => this.commands.hasOwnProperty(x))) { + console.trace('WARN: Duplicate slash command registered!'); + } + this.commands[command] = fnObj; if (Array.isArray(aliases)) { @@ -80,15 +90,67 @@ const registerSlashCommand = parser.addCommand.bind(parser); const getSlashCommandsHelp = parser.getHelpString.bind(parser); parser.addCommand('help', helpCommandCallback, ['?'], ' – displays this help message', true, true); -parser.addCommand('bg', setBackgroundCallback, ['background'], '(filename) – sets a background according to filename, partial names allowed, will set the first one alphebetically if multiple files begin with the provided argument string', false, true); -parser.addCommand('sys', sendNarratorMessage, [], ' – sends message as a system narrator', false, true); +parser.addCommand('bg', setBackgroundCallback, ['background'], '(filename) – sets a background according to filename, partial names allowed, will set the first one alphabetically if multiple files begin with the provided argument string', false, true); +parser.addCommand('sendas', sendMessageAs, [], ` – sends message as a specific character.
Example:
/sendas Chloe\nHello, guys!
will send "Hello, guys!" from "Chloe".
Uses character avatar if it exists in the characters list.`, true, true); +parser.addCommand('sys', sendNarratorMessage, [], '(text) – sends message as a system narrator', false, true); parser.addCommand('sysname', setNarratorName, [], '(name) – sets a name for future system narrator messages in this chat (display only). Default: System. Leave empty to reset.', true, true); const NARRATOR_NAME_KEY = 'narrator_name'; const NARRATOR_NAME_DEFAULT = 'System'; function setNarratorName(_, text) { - chat_metadata[NARRATOR_NAME_KEY] = text || NARRATOR_NAME_DEFAULT; + const name = text || NARRATOR_NAME_DEFAULT; + chat_metadata[NARRATOR_NAME_KEY] = name; + toastr.info(`System narrator name set to ${name}`); + saveChatConditional(); +} + +function sendMessageAs(_, text) { + if (!text) { + return; + } + + const parts = text.split('\n'); + + if (parts.length <= 1) { + toastr.warning('Both character name and message are required. Separate them with a new line.'); + return; + } + + const name = parts.shift().trim(); + const mesText = parts.join('\n').trim(); + // Messages that do nothing but set bias will be hidden from the context + const bias = extractMessageBias(mesText); + const isSystem = replaceBiasMarkup(mesText).trim().length === 0; + + const character = characters.find(x => x.name === name); + let force_avatar, original_avatar; + + if (character && character.avatar !== 'none') { + force_avatar = getThumbnailUrl('avatar', character.avatar); + original_avatar = character.avatar; + } + else { + force_avatar = default_avatar; + original_avatar = default_avatar; + } + + const message = { + name: name, + is_user: false, + is_name: true, + is_system: isSystem, + send_date: humanizedDateTime(), + mes: mesText, + force_avatar: force_avatar, + original_avatar: original_avatar, + extra: { + bias: bias.trim().length ? bias : null, + } + }; + + chat.push(message); + addOneMessage(message); saveChatConditional(); } @@ -98,21 +160,27 @@ function sendNarratorMessage(_, text) { } const name = chat_metadata[NARRATOR_NAME_KEY] || NARRATOR_NAME_DEFAULT; + // Messages that do nothing but set bias will be hidden from the context + const bias = extractMessageBias(text); + const isSystem = replaceBiasMarkup(text).trim().length === 0; + const message = { name: name, is_user: false, is_name: false, - is_system: false, + is_system: isSystem, send_date: humanizedDateTime(), mes: text.trim(), force_avatar: system_avatar, extra: { type: system_message_types.NARRATOR, + bias: bias.trim().length ? bias : null, }, }; chat.push(message); addOneMessage(message); + saveChatConditional(); } function helpCommandCallback() { diff --git a/public/scripts/toastr.js.map b/public/scripts/toastr.js.map new file mode 100644 index 000000000..07b5237f9 --- /dev/null +++ b/public/scripts/toastr.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["toastr.js"],"names":["define","$","error","message","title","optionsOverride","notify","type","toastType","iconClass","getOptions","iconClasses","getContainer","options","create","$container","containerId","length","createContainer","info","subscribe","callback","listener","success","warning","clear","$toastElement","clearOptions","clearToast","clearContainer","remove","removeToast","children","toastsToClear","i","force","hideMethod","duration","hideDuration","easing","hideEasing","complete","attr","addClass","positionClass","appendTo","target","getDefaults","tapToDismiss","toastClass","debug","showMethod","showDuration","showEasing","onShown","undefined","onHidden","closeMethod","closeDuration","closeEasing","closeOnHover","extendedTimeOut","timeOut","titleClass","messageClass","escapeHtml","closeHtml","closeClass","newestOnTop","preventDuplicates","progressBar","progressClass","rtl","publish","args","map","source","replace","personalizeToast","setIcon","setTitle","setMessage","setCloseButton","setProgressBar","setRTL","setSequence","setAria","ariaValue","handleEvents","hover","stickAround","delayedHideToast","onclick","click","hideToast","closeButton","$closeElement","event","stopPropagation","cancelBubble","onCloseClick","displayToast","hide","intervalId","setTimeout","maxHideTime","parseFloat","hideEta","Date","getTime","setInterval","updateProgress","prepend","append","suffix","$titleElement","$messageElement","$progressElement","shouldExit","previousToast","override","method","clearTimeout","response","state","endTime","stop","percentage","width","extend","toastId","startTime","console","log","toastr","is","version","amd","deps","factory","module","exports","require","window","jQuery"],"mappings":"CAaC,SAAUA,GACPA,GAAQ,UAAW,SAAUC,GACzB,MAAO,YA8BH,QAASC,GAAMC,EAASC,EAAOC,GAC3B,MAAOC,IACHC,KAAMC,EAAUN,MAChBO,UAAWC,IAAaC,YAAYT,MACpCC,QAASA,EACTE,gBAAiBA,EACjBD,MAAOA,IAIf,QAASQ,GAAaC,EAASC,GAG3B,MAFKD,KAAWA,EAAUH,KAC1BK,EAAad,EAAE,IAAMY,EAAQG,aACzBD,EAAWE,OACJF,GAEPD,IACAC,EAAaG,EAAgBL,IAE1BE,GAGX,QAASI,GAAKhB,EAASC,EAAOC,GAC1B,MAAOC,IACHC,KAAMC,EAAUW,KAChBV,UAAWC,IAAaC,YAAYQ,KACpChB,QAASA,EACTE,gBAAiBA,EACjBD,MAAOA,IAIf,QAASgB,GAAUC,GACfC,EAAWD,EAGf,QAASE,GAAQpB,EAASC,EAAOC,GAC7B,MAAOC,IACHC,KAAMC,EAAUe,QAChBd,UAAWC,IAAaC,YAAYY,QACpCpB,QAASA,EACTE,gBAAiBA,EACjBD,MAAOA,IAIf,QAASoB,GAAQrB,EAASC,EAAOC,GAC7B,MAAOC,IACHC,KAAMC,EAAUgB,QAChBf,UAAWC,IAAaC,YAAYa,QACpCrB,QAASA,EACTE,gBAAiBA,EACjBD,MAAOA,IAIf,QAASqB,GAAMC,EAAeC,GAC1B,GAAId,GAAUH,GACTK,IAAcH,EAAaC,GAC3Be,EAAWF,EAAeb,EAASc,IACpCE,EAAehB,GAIvB,QAASiB,GAAOJ,GACZ,GAAIb,GAAUH,GAEd,OADKK,IAAcH,EAAaC,GAC5Ba,GAAuD,IAAtCzB,EAAE,SAAUyB,GAAeT,WAC5Cc,GAAYL,QAGZX,EAAWiB,WAAWf,QACtBF,EAAWe,UAMnB,QAASD,GAAgBhB,GAErB,IAAK,GADDoB,GAAgBlB,EAAWiB,WACtBE,EAAID,EAAchB,OAAS,EAAGiB,GAAK,EAAGA,IAC3CN,EAAW3B,EAAEgC,EAAcC,IAAKrB,GAIxC,QAASe,GAAYF,EAAeb,EAASc,GACzC,GAAIQ,MAAQR,IAAgBA,EAAaQ,QAAQR,EAAaQ,KAC9D,UAAIT,IAAkBS,GAA+C,IAAtClC,EAAE,SAAUyB,GAAeT,UACtDS,EAAcb,EAAQuB,aAClBC,SAAUxB,EAAQyB,aAClBC,OAAQ1B,EAAQ2B,WAChBC,SAAU,WAAcV,EAAYL,OAEjC,GAKf,QAASR,GAAgBL,GAMrB,MALAE,GAAad,EAAE,UACVyC,KAAK,KAAM7B,EAAQG,aACnB2B,SAAS9B,EAAQ+B,eAEtB7B,EAAW8B,SAAS5C,EAAEY,EAAQiC,SACvB/B,EAGX,QAASgC,KACL,OACIC,cAAc,EACdC,WAAY,QACZjC,YAAa,kBACbkC,OAAO,EAEPC,WAAY,SACZC,aAAc,IACdC,WAAY,QACZC,QAASC,OACTnB,WAAY,UACZE,aAAc,IACdE,WAAY,QACZgB,SAAUD,OACVE,aAAa,EACbC,eAAe,EACfC,aAAa,EACbC,cAAc,EAEdC,gBAAiB,IACjBlD,aACIT,MAAO,cACPiB,KAAM,aACNI,QAAS,gBACTC,QAAS,iBAEbf,UAAW,aACXmC,cAAe,kBACfkB,QAAS,IACTC,WAAY,cACZC,aAAc,gBACdC,YAAY,EACZnB,OAAQ,OACRoB,UAAW,yCACXC,WAAY,qBACZC,aAAa,EACbC,mBAAmB,EACnBC,aAAa,EACbC,cAAe,iBACfC,KAAK,GAIb,QAASC,GAAQC,GACRpD,GACLA,EAASoD,GAGb,QAASpE,GAAOqE,GAgDZ,QAASV,GAAWW,GAKhB,MAJc,OAAVA,IACAA,EAAS,IAGNA,EACFC,QAAQ,KAAM,SACdA,QAAQ,KAAM,UACdA,QAAQ,KAAM,SACdA,QAAQ,KAAM,QACdA,QAAQ,KAAM,QAGvB,QAASC,KACLC,IACAC,IACAC,IACAC,IACAC,IACAC,IACAC,IACAC,IAGJ,QAASA,KACL,GAAIC,GAAY,EAChB,QAAQZ,EAAIlE,WACR,IAAK,gBACL,IAAK,aACD8E,EAAa,QACb,MACJ,SACIA,EAAY,YAEpB7D,EAAcgB,KAAK,YAAa6C,GAGpC,QAASC,KACD3E,EAAQ+C,cACRlC,EAAc+D,MAAMC,EAAaC,IAGhC9E,EAAQ+E,SAAW/E,EAAQmC,cAC5BtB,EAAcmE,MAAMC,GAGpBjF,EAAQkF,aAAeC,GACvBA,EAAcH,MAAM,SAAUI,GACtBA,EAAMC,gBACND,EAAMC,kBACwB3C,SAAvB0C,EAAME,cAA8BF,EAAME,gBAAiB,IAClEF,EAAME,cAAe,GAGrBtF,EAAQuF,cACRvF,EAAQuF,aAAaH,GAGzBH,GAAU,KAIdjF,EAAQ+E,SACRlE,EAAcmE,MAAM,SAAUI,GAC1BpF,EAAQ+E,QAAQK,GAChBH,MAKZ,QAASO,KACL3E,EAAc4E,OAEd5E,EAAcb,EAAQsC,aACjBd,SAAUxB,EAAQuC,aAAcb,OAAQ1B,EAAQwC,WAAYZ,SAAU5B,EAAQyC,UAG/EzC,EAAQiD,QAAU,IAClByC,EAAaC,WAAWV,EAAWjF,EAAQiD,SAC3CQ,EAAYmC,YAAcC,WAAW7F,EAAQiD,SAC7CQ,EAAYqC,SAAU,GAAIC,OAAOC,UAAYvC,EAAYmC,YACrD5F,EAAQyD,cACRA,EAAYiC,WAAaO,YAAYC,EAAgB,MAKjE,QAAShC,KACDJ,EAAIlE,WACJiB,EAAciB,SAAS9B,EAAQoC,YAAYN,SAASlC,GAI5D,QAAS4E,KACDxE,EAAQuD,YACRrD,EAAWiG,QAAQtF,GAEnBX,EAAWkG,OAAOvF,GAI1B,QAASsD,KACL,GAAIL,EAAIvE,MAAO,CACX,GAAI8G,GAASvC,EAAIvE,KACbS,GAAQoD,aACRiD,EAASjD,EAAWU,EAAIvE,QAE5B+G,EAAcF,OAAOC,GAAQvE,SAAS9B,EAAQkD,YAC9CrC,EAAcuF,OAAOE,IAI7B,QAASlC,KACL,GAAIN,EAAIxE,QAAS,CACb,GAAI+G,GAASvC,EAAIxE,OACbU,GAAQoD,aACRiD,EAASjD,EAAWU,EAAIxE,UAE5BiH,EAAgBH,OAAOC,GAAQvE,SAAS9B,EAAQmD,cAChDtC,EAAcuF,OAAOG,IAI7B,QAASlC,KACDrE,EAAQkF,cACRC,EAAcrD,SAAS9B,EAAQsD,YAAYzB,KAAK,OAAQ,UACxDhB,EAAcsF,QAAQhB,IAI9B,QAASb,KACDtE,EAAQyD,cACR+C,EAAiB1E,SAAS9B,EAAQ0D,eAClC7C,EAAcsF,QAAQK,IAI9B,QAASjC,KACDvE,EAAQ2D,KACR9C,EAAciB,SAAS,OAI/B,QAAS2E,GAAWzG,EAAS8D,GACzB,GAAI9D,EAAQwD,kBAAmB,CAC3B,GAAIM,EAAIxE,UAAYoH,EAChB,OAAO,CAEPA,GAAgB5C,EAAIxE,QAG5B,OAAO,EAGX,QAAS2F,GAAU0B,GACf,GAAIC,GAASD,GAAY3G,EAAQ4C,eAAgB,EAAQ5C,EAAQ4C,YAAc5C,EAAQuB,WACnFC,EAAWmF,GAAY3G,EAAQ6C,iBAAkB,EACjD7C,EAAQ6C,cAAgB7C,EAAQyB,aAChCC,EAASiF,GAAY3G,EAAQ8C,eAAgB,EAAQ9C,EAAQ8C,YAAc9C,EAAQ2B,UACvF,KAAIvC,EAAE,SAAUyB,GAAeT,QAAWuG,EAI1C,MADAE,cAAapD,EAAYiC,YAClB7E,EAAc+F,IACjBpF,SAAUA,EACVE,OAAQA,EACRE,SAAU,WACNV,EAAYL,GACZgG,aAAanB,GACT1F,EAAQ2C,UAA+B,WAAnBmE,EAASC,OAC7B/G,EAAQ2C,WAEZmE,EAASC,MAAQ,SACjBD,EAASE,QAAU,GAAIjB,MACvBnC,EAAQkD,MAKpB,QAAShC,MACD9E,EAAQiD,QAAU,GAAKjD,EAAQgD,gBAAkB,KACjD0C,EAAaC,WAAWV,EAAWjF,EAAQgD,iBAC3CS,EAAYmC,YAAcC,WAAW7F,EAAQgD,iBAC7CS,EAAYqC,SAAU,GAAIC,OAAOC,UAAYvC,EAAYmC,aAIjE,QAASf,KACLgC,aAAanB,GACbjC,EAAYqC,QAAU,EACtBjF,EAAcoG,MAAK,GAAM,GAAMjH,EAAQsC,aAClCd,SAAUxB,EAAQuC,aAAcb,OAAQ1B,EAAQwC,aAIzD,QAAS0D,KACL,GAAIgB,IAAezD,EAAYqC,SAAW,GAAIC,OAAOC,WAAcvC,EAAYmC,YAAe,GAC9FY,GAAiBW,MAAMD,EAAa,KApPxC,GAAIlH,GAAUH,IACVD,EAAYkE,EAAIlE,WAAaI,EAAQJ,SAOzC,IALqC,mBAAzBkE,GAAmB,kBAC3B9D,EAAUZ,EAAEgI,OAAOpH,EAAS8D,EAAItE,iBAChCI,EAAYkE,EAAItE,gBAAgBI,WAAaA,IAG7C6G,EAAWzG,EAAS8D,GAAxB,CAEAuD,IAEAnH,EAAaH,EAAaC,GAAS,EAEnC,IAAI0F,GAAa,KACb7E,EAAgBzB,EAAE,UAClBkH,EAAgBlH,EAAE,UAClBmH,EAAkBnH,EAAE,UACpBoH,EAAmBpH,EAAE,UACrB+F,EAAgB/F,EAAEY,EAAQqD,WAC1BI,GACAiC,WAAY,KACZI,QAAS,KACTF,YAAa,MAEbkB,GACAO,QAASA,EACTN,MAAO,UACPO,UAAW,GAAIvB,MACf/F,QAASA,EACT8D,IAAKA,EAeT,OAZAG,KAEAuB,IAEAb,IAEAf,EAAQkD,GAEJ9G,EAAQqC,OAASkF,SACjBA,QAAQC,IAAIV,GAGTjG,GA2MX,QAAShB,KACL,MAAOT,GAAEgI,UAAWlF,IAAeuF,EAAOzH,SAG9C,QAASkB,GAAYL,GACZX,IAAcA,EAAaH,KAC5Bc,EAAc6G,GAAG,cAGrB7G,EAAcI,SACdJ,EAAgB,KACqB,IAAjCX,EAAWiB,WAAWf,SACtBF,EAAWe,SACXyF,EAAgBhE,SA/bxB,GAAIxC,GACAO,EAsBAiG,EArBAW,EAAU,EACV1H,GACAN,MAAO,QACPiB,KAAM,OACNI,QAAS,UACTC,QAAS,WAGT8G,GACA7G,MAAOA,EACPK,OAAQA,EACR5B,MAAOA,EACPU,aAAcA,EACdO,KAAMA,EACNN,WACAO,UAAWA,EACXG,QAASA,EACTiH,QAAS,QACThH,QAASA,EAKb,OAAO8G,SA4aC,kBAAXtI,SAAyBA,OAAOyI,IAAMzI,OAAS,SAAU0I,EAAMC,GAC9C,mBAAXC,SAA0BA,OAAOC,QACxCD,OAAOC,QAAUF,EAAQG,QAAQ,WAEjCC,OAAOT,OAASK,EAAQI,OAAOC","file":"toastr.js","sourcesContent":["/*\n * Toastr\n * Copyright 2012-2015\n * Authors: John Papa, Hans Fjällemark, and Tim Ferrell.\n * All Rights Reserved.\n * Use, reproduction, distribution, and modification of this code is subject to the terms and\n * conditions of the MIT license, available at http://www.opensource.org/licenses/mit-license.php\n *\n * ARIA Support: Greta Krafsig\n *\n * Project: https://github.com/CodeSeven/toastr\n */\n/* global define */\n(function (define) {\n define(['jquery'], function ($) {\n return (function () {\n var $container;\n var listener;\n var toastId = 0;\n var toastType = {\n error: 'error',\n info: 'info',\n success: 'success',\n warning: 'warning'\n };\n\n var toastr = {\n clear: clear,\n remove: remove,\n error: error,\n getContainer: getContainer,\n info: info,\n options: {},\n subscribe: subscribe,\n success: success,\n version: '2.1.3',\n warning: warning\n };\n\n var previousToast;\n\n return toastr;\n\n ////////////////\n\n function error(message, title, optionsOverride) {\n return notify({\n type: toastType.error,\n iconClass: getOptions().iconClasses.error,\n message: message,\n optionsOverride: optionsOverride,\n title: title\n });\n }\n\n function getContainer(options, create) {\n if (!options) { options = getOptions(); }\n $container = $('#' + options.containerId);\n if ($container.length) {\n return $container;\n }\n if (create) {\n $container = createContainer(options);\n }\n return $container;\n }\n\n function info(message, title, optionsOverride) {\n return notify({\n type: toastType.info,\n iconClass: getOptions().iconClasses.info,\n message: message,\n optionsOverride: optionsOverride,\n title: title\n });\n }\n\n function subscribe(callback) {\n listener = callback;\n }\n\n function success(message, title, optionsOverride) {\n return notify({\n type: toastType.success,\n iconClass: getOptions().iconClasses.success,\n message: message,\n optionsOverride: optionsOverride,\n title: title\n });\n }\n\n function warning(message, title, optionsOverride) {\n return notify({\n type: toastType.warning,\n iconClass: getOptions().iconClasses.warning,\n message: message,\n optionsOverride: optionsOverride,\n title: title\n });\n }\n\n function clear($toastElement, clearOptions) {\n var options = getOptions();\n if (!$container) { getContainer(options); }\n if (!clearToast($toastElement, options, clearOptions)) {\n clearContainer(options);\n }\n }\n\n function remove($toastElement) {\n var options = getOptions();\n if (!$container) { getContainer(options); }\n if ($toastElement && $(':focus', $toastElement).length === 0) {\n removeToast($toastElement);\n return;\n }\n if ($container.children().length) {\n $container.remove();\n }\n }\n\n // internal functions\n\n function clearContainer (options) {\n var toastsToClear = $container.children();\n for (var i = toastsToClear.length - 1; i >= 0; i--) {\n clearToast($(toastsToClear[i]), options);\n }\n }\n\n function clearToast ($toastElement, options, clearOptions) {\n var force = clearOptions && clearOptions.force ? clearOptions.force : false;\n if ($toastElement && (force || $(':focus', $toastElement).length === 0)) {\n $toastElement[options.hideMethod]({\n duration: options.hideDuration,\n easing: options.hideEasing,\n complete: function () { removeToast($toastElement); }\n });\n return true;\n }\n return false;\n }\n\n function createContainer(options) {\n $container = $('
')\n .attr('id', options.containerId)\n .addClass(options.positionClass);\n\n $container.appendTo($(options.target));\n return $container;\n }\n\n function getDefaults() {\n return {\n tapToDismiss: true,\n toastClass: 'toast',\n containerId: 'toast-container',\n debug: false,\n\n showMethod: 'fadeIn', //fadeIn, slideDown, and show are built into jQuery\n showDuration: 300,\n showEasing: 'swing', //swing and linear are built into jQuery\n onShown: undefined,\n hideMethod: 'fadeOut',\n hideDuration: 1000,\n hideEasing: 'swing',\n onHidden: undefined,\n closeMethod: false,\n closeDuration: false,\n closeEasing: false,\n closeOnHover: true,\n\n extendedTimeOut: 1000,\n iconClasses: {\n error: 'toast-error',\n info: 'toast-info',\n success: 'toast-success',\n warning: 'toast-warning'\n },\n iconClass: 'toast-info',\n positionClass: 'toast-top-right',\n timeOut: 5000, // Set timeOut and extendedTimeOut to 0 to make it sticky\n titleClass: 'toast-title',\n messageClass: 'toast-message',\n escapeHtml: false,\n target: 'body',\n closeHtml: '',\n closeClass: 'toast-close-button',\n newestOnTop: true,\n preventDuplicates: false,\n progressBar: false,\n progressClass: 'toast-progress',\n rtl: false\n };\n }\n\n function publish(args) {\n if (!listener) { return; }\n listener(args);\n }\n\n function notify(map) {\n var options = getOptions();\n var iconClass = map.iconClass || options.iconClass;\n\n if (typeof (map.optionsOverride) !== 'undefined') {\n options = $.extend(options, map.optionsOverride);\n iconClass = map.optionsOverride.iconClass || iconClass;\n }\n\n if (shouldExit(options, map)) { return; }\n\n toastId++;\n\n $container = getContainer(options, true);\n\n var intervalId = null;\n var $toastElement = $('
');\n var $titleElement = $('
');\n var $messageElement = $('
');\n var $progressElement = $('
');\n var $closeElement = $(options.closeHtml);\n var progressBar = {\n intervalId: null,\n hideEta: null,\n maxHideTime: null\n };\n var response = {\n toastId: toastId,\n state: 'visible',\n startTime: new Date(),\n options: options,\n map: map\n };\n\n personalizeToast();\n\n displayToast();\n\n handleEvents();\n\n publish(response);\n\n if (options.debug && console) {\n console.log(response);\n }\n\n return $toastElement;\n\n function escapeHtml(source) {\n if (source == null) {\n source = '';\n }\n\n return source\n .replace(/&/g, '&')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\n .replace(//g, '>');\n }\n\n function personalizeToast() {\n setIcon();\n setTitle();\n setMessage();\n setCloseButton();\n setProgressBar();\n setRTL();\n setSequence();\n setAria();\n }\n\n function setAria() {\n var ariaValue = '';\n switch (map.iconClass) {\n case 'toast-success':\n case 'toast-info':\n ariaValue = 'polite';\n break;\n default:\n ariaValue = 'assertive';\n }\n $toastElement.attr('aria-live', ariaValue);\n }\n\n function handleEvents() {\n if (options.closeOnHover) {\n $toastElement.hover(stickAround, delayedHideToast);\n }\n\n if (!options.onclick && options.tapToDismiss) {\n $toastElement.click(hideToast);\n }\n\n if (options.closeButton && $closeElement) {\n $closeElement.click(function (event) {\n if (event.stopPropagation) {\n event.stopPropagation();\n } else if (event.cancelBubble !== undefined && event.cancelBubble !== true) {\n event.cancelBubble = true;\n }\n\n if (options.onCloseClick) {\n options.onCloseClick(event);\n }\n\n hideToast(true);\n });\n }\n\n if (options.onclick) {\n $toastElement.click(function (event) {\n options.onclick(event);\n hideToast();\n });\n }\n }\n\n function displayToast() {\n $toastElement.hide();\n\n $toastElement[options.showMethod](\n {duration: options.showDuration, easing: options.showEasing, complete: options.onShown}\n );\n\n if (options.timeOut > 0) {\n intervalId = setTimeout(hideToast, options.timeOut);\n progressBar.maxHideTime = parseFloat(options.timeOut);\n progressBar.hideEta = new Date().getTime() + progressBar.maxHideTime;\n if (options.progressBar) {\n progressBar.intervalId = setInterval(updateProgress, 10);\n }\n }\n }\n\n function setIcon() {\n if (map.iconClass) {\n $toastElement.addClass(options.toastClass).addClass(iconClass);\n }\n }\n\n function setSequence() {\n if (options.newestOnTop) {\n $container.prepend($toastElement);\n } else {\n $container.append($toastElement);\n }\n }\n\n function setTitle() {\n if (map.title) {\n var suffix = map.title;\n if (options.escapeHtml) {\n suffix = escapeHtml(map.title);\n }\n $titleElement.append(suffix).addClass(options.titleClass);\n $toastElement.append($titleElement);\n }\n }\n\n function setMessage() {\n if (map.message) {\n var suffix = map.message;\n if (options.escapeHtml) {\n suffix = escapeHtml(map.message);\n }\n $messageElement.append(suffix).addClass(options.messageClass);\n $toastElement.append($messageElement);\n }\n }\n\n function setCloseButton() {\n if (options.closeButton) {\n $closeElement.addClass(options.closeClass).attr('role', 'button');\n $toastElement.prepend($closeElement);\n }\n }\n\n function setProgressBar() {\n if (options.progressBar) {\n $progressElement.addClass(options.progressClass);\n $toastElement.prepend($progressElement);\n }\n }\n\n function setRTL() {\n if (options.rtl) {\n $toastElement.addClass('rtl');\n }\n }\n\n function shouldExit(options, map) {\n if (options.preventDuplicates) {\n if (map.message === previousToast) {\n return true;\n } else {\n previousToast = map.message;\n }\n }\n return false;\n }\n\n function hideToast(override) {\n var method = override && options.closeMethod !== false ? options.closeMethod : options.hideMethod;\n var duration = override && options.closeDuration !== false ?\n options.closeDuration : options.hideDuration;\n var easing = override && options.closeEasing !== false ? options.closeEasing : options.hideEasing;\n if ($(':focus', $toastElement).length && !override) {\n return;\n }\n clearTimeout(progressBar.intervalId);\n return $toastElement[method]({\n duration: duration,\n easing: easing,\n complete: function () {\n removeToast($toastElement);\n clearTimeout(intervalId);\n if (options.onHidden && response.state !== 'hidden') {\n options.onHidden();\n }\n response.state = 'hidden';\n response.endTime = new Date();\n publish(response);\n }\n });\n }\n\n function delayedHideToast() {\n if (options.timeOut > 0 || options.extendedTimeOut > 0) {\n intervalId = setTimeout(hideToast, options.extendedTimeOut);\n progressBar.maxHideTime = parseFloat(options.extendedTimeOut);\n progressBar.hideEta = new Date().getTime() + progressBar.maxHideTime;\n }\n }\n\n function stickAround() {\n clearTimeout(intervalId);\n progressBar.hideEta = 0;\n $toastElement.stop(true, true)[options.showMethod](\n {duration: options.showDuration, easing: options.showEasing}\n );\n }\n\n function updateProgress() {\n var percentage = ((progressBar.hideEta - (new Date().getTime())) / progressBar.maxHideTime) * 100;\n $progressElement.width(percentage + '%');\n }\n }\n\n function getOptions() {\n return $.extend({}, getDefaults(), toastr.options);\n }\n\n function removeToast($toastElement) {\n if (!$container) { $container = getContainer(); }\n if ($toastElement.is(':visible')) {\n return;\n }\n $toastElement.remove();\n $toastElement = null;\n if ($container.children().length === 0) {\n $container.remove();\n previousToast = undefined;\n }\n }\n\n })();\n });\n}(typeof define === 'function' && define.amd ? define : function (deps, factory) {\n if (typeof module !== 'undefined' && module.exports) { //Node\n module.exports = factory(require('jquery'));\n } else {\n window.toastr = factory(window.jQuery);\n }\n}));\n"],"sourceRoot":"/source/"} \ No newline at end of file diff --git a/public/scripts/toastr.min.js b/public/scripts/toastr.min.js new file mode 100644 index 000000000..4b5f34a05 --- /dev/null +++ b/public/scripts/toastr.min.js @@ -0,0 +1,7 @@ +/* + * Note that this is toastr v2.1.3, the "latest" version in url has no more maintenance, + * please go to https://cdnjs.com/libraries/toastr.js and pick a certain version you want to use, + * make sure you copy the url from the website since the url may change between versions. + * */ +!function(e){e(["jquery"],function(e){return function(){function t(e,t,n){return g({type:O.error,iconClass:m().iconClasses.error,message:e,optionsOverride:n,title:t})}function n(t,n){return t||(t=m()),v=e("#"+t.containerId),v.length?v:(n&&(v=d(t)),v)}function o(e,t,n){return g({type:O.info,iconClass:m().iconClasses.info,message:e,optionsOverride:n,title:t})}function s(e){C=e}function i(e,t,n){return g({type:O.success,iconClass:m().iconClasses.success,message:e,optionsOverride:n,title:t})}function a(e,t,n){return g({type:O.warning,iconClass:m().iconClasses.warning,message:e,optionsOverride:n,title:t})}function r(e,t){var o=m();v||n(o),u(e,o,t)||l(o)}function c(t){var o=m();return v||n(o),t&&0===e(":focus",t).length?void h(t):void(v.children().length&&v.remove())}function l(t){for(var n=v.children(),o=n.length-1;o>=0;o--)u(e(n[o]),t)}function u(t,n,o){var s=!(!o||!o.force)&&o.force;return!(!t||!s&&0!==e(":focus",t).length)&&(t[n.hideMethod]({duration:n.hideDuration,easing:n.hideEasing,complete:function(){h(t)}}),!0)}function d(t){return v=e("
").attr("id",t.containerId).addClass(t.positionClass),v.appendTo(e(t.target)),v}function p(){return{tapToDismiss:!0,toastClass:"toast",containerId:"toast-container",debug:!1,showMethod:"fadeIn",showDuration:300,showEasing:"swing",onShown:void 0,hideMethod:"fadeOut",hideDuration:1e3,hideEasing:"swing",onHidden:void 0,closeMethod:!1,closeDuration:!1,closeEasing:!1,closeOnHover:!0,extendedTimeOut:1e3,iconClasses:{error:"toast-error",info:"toast-info",success:"toast-success",warning:"toast-warning"},iconClass:"toast-info",positionClass:"toast-top-right",timeOut:5e3,titleClass:"toast-title",messageClass:"toast-message",escapeHtml:!1,target:"body",closeHtml:'',closeClass:"toast-close-button",newestOnTop:!0,preventDuplicates:!1,progressBar:!1,progressClass:"toast-progress",rtl:!1}}function f(e){C&&C(e)}function g(t){function o(e){return null==e&&(e=""),e.replace(/&/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">")}function s(){c(),u(),d(),p(),g(),C(),l(),i()}function i(){var e="";switch(t.iconClass){case"toast-success":case"toast-info":e="polite";break;default:e="assertive"}I.attr("aria-live",e)}function a(){E.closeOnHover&&I.hover(H,D),!E.onclick&&E.tapToDismiss&&I.click(b),E.closeButton&&j&&j.click(function(e){e.stopPropagation?e.stopPropagation():void 0!==e.cancelBubble&&e.cancelBubble!==!0&&(e.cancelBubble=!0),E.onCloseClick&&E.onCloseClick(e),b(!0)}),E.onclick&&I.click(function(e){E.onclick(e),b()})}function r(){I.hide(),I[E.showMethod]({duration:E.showDuration,easing:E.showEasing,complete:E.onShown}),E.timeOut>0&&(k=setTimeout(b,E.timeOut),F.maxHideTime=parseFloat(E.timeOut),F.hideEta=(new Date).getTime()+F.maxHideTime,E.progressBar&&(F.intervalId=setInterval(x,10)))}function c(){t.iconClass&&I.addClass(E.toastClass).addClass(y)}function l(){E.newestOnTop?v.prepend(I):v.append(I)}function u(){if(t.title){var e=t.title;E.escapeHtml&&(e=o(t.title)),M.append(e).addClass(E.titleClass),I.append(M)}}function d(){if(t.message){var e=t.message;E.escapeHtml&&(e=o(t.message)),B.append(e).addClass(E.messageClass),I.append(B)}}function p(){E.closeButton&&(j.addClass(E.closeClass).attr("role","button"),I.prepend(j))}function g(){E.progressBar&&(q.addClass(E.progressClass),I.prepend(q))}function C(){E.rtl&&I.addClass("rtl")}function O(e,t){if(e.preventDuplicates){if(t.message===w)return!0;w=t.message}return!1}function b(t){var n=t&&E.closeMethod!==!1?E.closeMethod:E.hideMethod,o=t&&E.closeDuration!==!1?E.closeDuration:E.hideDuration,s=t&&E.closeEasing!==!1?E.closeEasing:E.hideEasing;if(!e(":focus",I).length||t)return clearTimeout(F.intervalId),I[n]({duration:o,easing:s,complete:function(){h(I),clearTimeout(k),E.onHidden&&"hidden"!==P.state&&E.onHidden(),P.state="hidden",P.endTime=new Date,f(P)}})}function D(){(E.timeOut>0||E.extendedTimeOut>0)&&(k=setTimeout(b,E.extendedTimeOut),F.maxHideTime=parseFloat(E.extendedTimeOut),F.hideEta=(new Date).getTime()+F.maxHideTime)}function H(){clearTimeout(k),F.hideEta=0,I.stop(!0,!0)[E.showMethod]({duration:E.showDuration,easing:E.showEasing})}function x(){var e=(F.hideEta-(new Date).getTime())/F.maxHideTime*100;q.width(e+"%")}var E=m(),y=t.iconClass||E.iconClass;if("undefined"!=typeof t.optionsOverride&&(E=e.extend(E,t.optionsOverride),y=t.optionsOverride.iconClass||y),!O(E,t)){T++,v=n(E,!0);var k=null,I=e("
"),M=e("
"),B=e("
"),q=e("
"),j=e(E.closeHtml),F={intervalId:null,hideEta:null,maxHideTime:null},P={toastId:T,state:"visible",startTime:new Date,options:E,map:t};return s(),r(),a(),f(P),E.debug&&console&&console.log(P),I}}function m(){return e.extend({},p(),b.options)}function h(e){v||(v=n()),e.is(":visible")||(e.remove(),e=null,0===v.children().length&&(v.remove(),w=void 0))}var v,C,w,T=0,O={error:"error",info:"info",success:"success",warning:"warning"},b={clear:r,remove:c,error:t,getContainer:n,info:o,options:{},subscribe:s,success:i,version:"2.1.3",warning:a};return b}()})}("function"==typeof define&&define.amd?define:function(e,t){"undefined"!=typeof module&&module.exports?module.exports=t(require("jquery")):window.toastr=t(window.jQuery)}); +//# sourceMappingURL=toastr.js.map diff --git a/public/scripts/utils.js b/public/scripts/utils.js index dc84a357d..77763b536 100644 --- a/public/scripts/utils.js +++ b/public/scripts/utils.js @@ -216,3 +216,19 @@ export function end_trim_to_sentence(input, include_newline = false) { return input.substring(0, last + 1).trimEnd(); } + +export function countOccurrences(string, character) { + let count = 0; + + for (let i = 0; i < string.length; i++) { + if (string[i] === character) { + count++; + } + } + + return count; +} + +export function isOdd(number) { + return number % 2 !== 0; +} \ No newline at end of file diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js index 7911cb5ed..20277f10b 100644 --- a/public/scripts/world-info.js +++ b/public/scripts/world-info.js @@ -7,6 +7,7 @@ export { world_info_budget, world_info_depth, world_info_recursive, + world_info_case_sensitive, world_names, imported_world_name, checkWorldInfo, @@ -23,6 +24,7 @@ let world_info_depth = 2; let world_info_budget = 128; let is_world_edit_open = false; let world_info_recursive = false; +let world_info_case_sensitive = false; let imported_world_name = ""; const saveWorldDebounced = debounce(async () => await _save(), 500); const saveSettingsDebounced = debounce(() => saveSettings(), 500); @@ -51,6 +53,8 @@ function setWorldInfoSettings(settings, data) { world_info_budget = Number(settings.world_info_budget); if (settings.world_info_recursive !== undefined) world_info_recursive = Boolean(settings.world_info_recursive); + if (settings.world_info_case_sensitive !== undefined) + world_info_case_sensitive = Boolean(settings.world_info_case_sensitive); $("#world_info_depth_counter").text(world_info_depth); $("#world_info_depth").val(world_info_depth); @@ -59,6 +63,7 @@ function setWorldInfoSettings(settings, data) { $("#world_info_budget").val(world_info_budget); $("#world_info_recursive").prop('checked', world_info_recursive); + $("#world_info_case_sensitive").prop('checked', world_info_case_sensitive); world_names = data.world_names?.length ? data.world_names : []; @@ -80,7 +85,7 @@ function setWorldInfoSettings(settings, data) { // World Info Editor async function showWorldEditor() { if (!world_info) { - callPopup("

Select a world info first!

", "text"); + toastr.warning("Select a world info first!"); return; } @@ -476,13 +481,18 @@ async function createNewWorldInfo() { } } +// Gets a string that respects the case sensitivity setting +function transformString(str) { + return world_info_case_sensitive ? str : str.toLowerCase(); +} + function checkWorldInfo(chat) { if (world_info_data.entries.length == 0) { return ""; } const messagesToLookBack = world_info_depth * 2; - let textToScan = chat.slice(0, messagesToLookBack).join("").toLowerCase(); + let textToScan = transformString(chat.slice(0, messagesToLookBack).join("")); let worldInfoBefore = ""; let worldInfoAfter = ""; let needsToScan = true; @@ -506,16 +516,18 @@ function checkWorldInfo(chat) { if (Array.isArray(entry.key) && entry.key.length) { primary: for (let key of entry.key) { - if (key && textToScan.includes(key.trim().toLowerCase())) { + const substituted = substituteParams(key); + if (substituted && textToScan.includes(transformString(substituted.trim()))) { if ( entry.selective && Array.isArray(entry.keysecondary) && entry.keysecondary.length ) { secondary: for (let keysecondary of entry.keysecondary) { + const secondarySubstituted = substituteParams(keysecondary); if ( - keysecondary && - textToScan.includes(keysecondary.trim().toLowerCase()) + secondarySubstituted && + textToScan.includes(transformString(secondarySubstituted.trim())) ) { activatedNow.add(entry.uid); break secondary; @@ -555,11 +567,7 @@ function checkWorldInfo(chat) { } if (needsToScan) { - textToScan = - newEntries - .map((x) => x.content) - .join("\n") - .toLowerCase() + textToScan; + textToScan = (transformString(newEntries.map(x => x.content).join('\n')) + textToScan); } allActivatedEntries = new Set([...allActivatedEntries, ...activatedNow]); @@ -581,7 +589,7 @@ function selectImportedWorldInfo() { imported_world_name = ""; } -$(document).ready(() => { +jQuery(() => { $("#world_info").change(async function () { const selectedWorld = $("#world_info").find(":selected").val(); world_info = null; @@ -686,4 +694,9 @@ $(document).ready(() => { world_info_recursive = !!$(this).prop('checked'); saveSettingsDebounced(); }) -}); \ No newline at end of file + + $('#world_info_case_sensitive').on('input', function () { + world_info_case_sensitive = !!$(this).prop('checked'); + saveSettingsDebounced(); + }) +}); diff --git a/public/style.css b/public/style.css index 139a7b626..ff8bbfb6d 100644 --- a/public/style.css +++ b/public/style.css @@ -103,10 +103,12 @@ body { ::-webkit-scrollbar-thumb { background-color: var(--grey7070a); - border: 2px solid transparent; box-shadow: inset 0 0 0 1px var(--black50a); border-radius: 10px; background-clip: content-box; + border: 2px solid transparent; + border-top: 20px solid transparent; + min-height: 40px; } table.responsiveTable { @@ -132,6 +134,33 @@ table.responsiveTable { border-top: 2px solid grey; } +.animated { + -webkit-animation-duration: 3s !important; + animation-duration: 3s !important; + -webkit-animation-fill-mode: both !important; + animation-fill-mode: both !important; + box-shadow: inset 0 0 5px var(--SmartThemeQuoteColor); +} + +@keyframes flash { + + 20%, + 60%, + 100% { + opacity: 1; + } + + 0%, + 40%, + 80% { + opacity: 0.2; + } +} + +.flash { + animation-name: flash; +} + .tokenItemizingSubclass { font-size: calc(var(--mainFontSize) * 0.8); color: var(--SmartThemeEmColor); @@ -219,12 +248,12 @@ body.tts .mes_narrate { code { font-family: Consolas, monospace; white-space: pre-wrap; - word-wrap: break-word; + /* word-wrap: break-word; */ border: 1px solid var(--white30a); border-radius: 5px; background-color: var(--black70a); padding: 0 3px; - max-width: calc(100svw - 95px); + /* max-width: calc(100svw - 95px); */ line-height: var(--mainFontSize); } @@ -398,19 +427,6 @@ code { justify-content: center; } -#token_breakdown div { - display: flex; - width: 100%; - justify-content: center; -} - -.token_breakdown_segment { - min-width: 40px !important; - border: solid 2px; - border-radius: 5px; -} - - #loading_mes { display: none; width: 40px; @@ -500,8 +516,6 @@ code { display: flex; align-items: center; justify-content: center; - display: none; - } #extensionsMenuButton:hover { @@ -540,6 +554,16 @@ code { cursor: pointer; } +#extensionsMenu>div { + transition: 0.3s; + opacity: 0.7; +} + +#extensionsMenu>div:hover { + filter: brightness(1.2); + opacity: 1; +} + .options-content a div:first-child { min-width: 20px; } @@ -564,12 +588,13 @@ code { } .mes { - display: grid; - grid-template-columns: min-content min-content auto min-content; + display: flex; + align-items: flex-start; padding: 20px 10px 0 10px; margin-top: 0; width: 100%; color: var(--SmartThemeBodyColor); + position: relative; } .mes q:before, @@ -579,9 +604,6 @@ code { } .last_mes { - grid-template-columns: [checkbox] fit-content(60px) [avatar-leftswipe] 50px [name-mestext] auto [edit-rightswipe] 30px !important; - grid-template-rows: [avatar-NameMesText-edit] 50px [swipes] auto; - grid-row-gap: 30px; margin-bottom: 0 !important; /*only affects bubblechat to make it sit nicely at the bottom*/ } @@ -593,7 +615,6 @@ code { height: 40px; width: 40px; opacity: 0.3; - right: 5px; align-items: center; justify-content: center; z-index: 9999; @@ -602,12 +623,9 @@ code { flex-flow: column; font-size: 30px; cursor: pointer; -} - -.swipe_right img, -.swipe_left img { - height: 30px; - width: 30px; + align-self: center; + position: absolute; + bottom: 15px; } .swipes-counter { @@ -620,11 +638,12 @@ code { } .swipe_left { - left: 15px; right: auto; - grid-column-start: 2; - align-items: flex-end; - padding-bottom: 17px; + left: 20px; +} + +.swipe_right { + right: 5px; } .ui-settings { @@ -660,9 +679,12 @@ code { border-style: none; } +.last_mes .mesAvatarWrapper { + padding-bottom: 50px; +} + .mes .avatar { cursor: pointer; - } .hotswapAvatar, @@ -754,11 +776,6 @@ body.big-avatars #user_avatar_block .avatar img { width: 60px; } -body.big-avatars .last_mes { - grid-template-rows: [avatar-NameMesText-edit] 80px [swipes] auto; - grid-template-columns: [checkbox] fit-content(60px) [avatar-leftswipe] fit-content(60px) [name-mestext] auto [edit-rightswipe] 30px !important; -} - body.big-avatars .avatar img { width: 100%; height: 100%; @@ -771,9 +788,7 @@ body.big-avatars .avatar img { .mes_block { padding-top: 0; padding-left: 10px; - grid-row-start: 1; - grid-row-end: 3; - grid-column-start: 3; + width: 100%; } @@ -893,6 +908,26 @@ select { } +.chat_injections_list:empty { + width: 100%; + height: 100%; +} + +.chat_injections_list:empty::before { + display: flex; + align-items: center; + justify-content: center; + content: "No injections"; + font-weight: bolder; + width: 100%; + height: 100%; + opacity: 0.8; + min-height: 3rem; +} + +.template_parameters_list code { + cursor: pointer; +} h3 { margin: 10px 0; @@ -1227,6 +1262,14 @@ input[type=search]:focus::-webkit-search-cancel-button { font-weight: bolder; } +.justifySpaceBetween { + justify-content: space-between; +} + +.mes_block .ch_name { + max-width: 100%; +} + /*applies to both groups and solos chars in the char list*/ #rm_print_characters_block .ch_name { width: 100%; @@ -1304,13 +1347,28 @@ input[type=search]:focus::-webkit-search-cancel-button { margin: 5px; cursor: pointer; aspect-ratio: 16/9; + justify-content: flex-end; +} + +.BGSampleTitle { + display: flex; + width: 100%; + height: min-content; + text-align: center; + justify-content: center; + align-self: flex-end; + bottom: 0; + position: relative; + word-break: break-word; + background-color: var(--black50a); + font-size: calc(var(--fontScale) * 0.9em); } .bg_example_cross { width: 15px; height: 15px; position: relative; - float: right; + /* float: right; */ right: 10px; top: 5px; cursor: pointer; @@ -1808,10 +1866,6 @@ input[type=search]:focus::-webkit-search-cancel-button { gap: 5px; } -#form_rename_world { - /* margin-right: 50px; */ -} - #form_rename_chat { flex: 1; } @@ -2222,11 +2276,21 @@ input[type="range"]::-webkit-slider-thumb { } .mes_buttons { - float: right; height: 20px; grid-row-start: 1; position: relative; - right: 0px; + display: flex; + gap: 10px; + flex-wrap: nowrap; + justify-content: flex-end; +} + +.last_mes .mes_buttons { + right: -30px; +} + +.last_mes .mes_block { + margin-right: 30px; } .mes_prompt, @@ -2236,8 +2300,6 @@ input[type="range"]::-webkit-slider-thumb { .mes_edit { cursor: pointer; transition: 0.3s ease-in-out; - height: 20px; - width: 20px; filter: drop-shadow(0px 0px 2px black); opacity: 0.2; } @@ -2250,24 +2312,6 @@ input[type="range"]::-webkit-slider-thumb { opacity: 1; } -.last_mes .sd_message_gen, -.last_mes .mes_copy, -.last_mes .mes_narrate, -.last_mes .mes_prompt { - grid-row-start: 1; - position: relative; - right: -30px; -} - - -.last_mes .mes_edit, -.last_mes .mes_edit_buttons, -.last_mes .mes_stop { - grid-row-start: 1; - position: relative; - right: -30px; -} - .mes_edit_buttons { display: none; flex-direction: row; @@ -3565,7 +3609,7 @@ label[for="extensions_autoconnect"] { .code-copied { position: absolute; - z-index: 99; + z-index: 10000; font-size: var(--mainFontSize); color: var(--SmartThemeBodyColor); background-color: var(--SmartThemeFastUIBGColor); @@ -3623,7 +3667,6 @@ label[for="extensions_autoconnect"] { backdrop-filter: blur(calc(var(--SmartThemeBlurStrength))); -webkit-backdrop-filter: blur(calc(var(--SmartThemeBlurStrength))); z-index: 9999 !important; - border: 1px solid var(--white30a); } .fillRight, @@ -3708,6 +3751,10 @@ toolcool-color-picker { flex: 1; } +.flex2 { + flex: 2; +} + .flexFlowColumn { flex-flow: column; } @@ -3995,16 +4042,6 @@ body.movingUI #floatingPrompt { resize: both; } -body.movingUI #chat::-webkit-scrollbar-thumb { - background-color: var(--grey7070a); - border: 2px solid transparent; - border-top: 20px solid transparent; - box-shadow: inset 0 0 0 1px var(--black50a); - border-radius: 10px; - background-clip: content-box; -} - - #expression-image.default, #expression-holder:has(.default) { height: 120px; diff --git a/readme.md b/readme.md index 8e0caa8f3..0f055f31a 100644 --- a/readme.md +++ b/readme.md @@ -65,6 +65,8 @@ Get in touch with the developers directly: * Character emotional expressions * Auto-Summary of the chat history * 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) ## UI Extensions 🚀 @@ -76,6 +78,8 @@ Get in touch with the developers directly: | D&D Dice | A set of 7 classic D&D dice for all your dice rolling needs.

*I used to roll the dice.
Feel the fear in my enemies' eyes* | None | image | | Author's Note | Built-in extension that allows you to append notes that will be added to the context and steer the story and character in a specific direction. Because it's sent after the character description, it has a lot of weight. Thanks Ali឵#2222 for pitching the idea! | None | ![image](https://user-images.githubusercontent.com/128647114/230311637-d809cd9b-af66-4dd1-a310-7a27e847c011.png) | | Character Backgrounds | Built-in extension to assign unique backgrounds to specific chats or groups. | None | image | +| Stable Diffusion | Use local of cloud-based Stable Diffusion webUI API to generate images. 5 presets included ('you', 'your face', 'me', 'the story', and 'the last message'. Free mode also supported via `/sd (anything_here_)` command in the chat input bar. Most common StableDiffusion generation settings are customizable within the SillyTavern UI. | None | image | +| Text-to-Speech | AI-generated voice will read back character messages on demand, or automatically read new messages they arrive. Supports ElevenLabs, Silero, and your device's TTS service. | None | image | ## UI/CSS/Quality of Life tweaks by RossAscends @@ -144,10 +148,10 @@ Easy to follow guide with pretty pictures: Installing via zip download - 1. install [NodeJS](https://nodejs.org/en) (latest LTS version is recommended) - 2. download the zip from this GitHub repo - 3. unzip it into a folder of your choice - 4. run start.bat via double-clicking or in a command line. + 1. Install [NodeJS](https://nodejs.org/en) (latest LTS version is recommended) + 2. Download the zip from this GitHub repo. (Get the `Source code (zip)` from [Releases](https://github.com/Cohee1207/SillyTavern/releases/latest)) + 3. Unzip it into a folder of your choice + 4. Run `Start.bat` via double-clicking or in a command line. 5. Once the server has prepared everything for you, it will open a tab in your browser. ### Linux @@ -203,21 +207,31 @@ Now devices which have the IP specified in the file will be able to connect. *Note: `config.conf` also has a `whitelist` array, which you can use in the same way, but this array will be ignored if `whitelist.txt` exists.* -### 2. Connecting to ST from a remote device +### 2. Getting the IP for the ST host machine -After the whitelist has been setup, to connect over wifi you'll need the IP of the ST-hosting device. +After the whitelist has been setup, you'll need the IP of the ST-hosting device. -If the ST-hosting device is on the same wifi network, you will point your remote device's browser to the ST-host's internal wifi IP: +If the ST-hosting device is on the same wifi network, you will use the ST-host's internal wifi IP: * For Windows: windows button > type `cmd.exe` in the search bar > type `ipconfig` in the console, hit Enter > look for `IPv4` listing. If you (or someone else) wants to connect to your hosted ST while not being on the same network, you will need the public IP of your ST-hosting device. -While using the ST-hosting device, access [this page](https://whatismyipaddress.com/) and look for for `IPv4`. This is what you would use to connect from the remote device. +* While using the ST-hosting device, access [this page](https://whatismyipaddress.com/) and look for for `IPv4`. This is what you would use to connect from the remote device. + +### 3. Connect the remote device to the ST host machine. + +Whatever IP you ended up with for your situation, you will put that IP address and port number into the remote device's web browser. + +A typical address for an ST host on the same wifi network would look like: + +`http://192.168.0.5:8000` + +Use http:// NOT https:// ### Opening your ST to all IPs -We do not reccomend doing this, but you can open `config.conf` and change `whitelist` to `false`. +We do not recommend doing this, but you can open `config.conf` and change `whitelist` to `false`. You must remove (or rename) `whitelist.txt` in the SillyTavern base install folder, if it exists. diff --git a/server.js b/server.js index 189b42076..96522bbc4 100644 --- a/server.js +++ b/server.js @@ -63,7 +63,9 @@ const utf8Encode = new TextEncoder(); const utf8Decode = new TextDecoder('utf-8', { ignoreBOM: true }); const commandExistsSync = require('command-exists').sync; +const characterCardParser = require('./src/character-card-parser.js'); const config = require(path.join(process.cwd(), './config.conf')); + const server_port = process.env.SILLY_TAVERN_PORT || config.port; const whitelistPath = path.join(process.cwd(), "./whitelist.txt"); @@ -123,7 +125,7 @@ let response_generate_openai; let response_getstatus_openai; //RossAscends: Added function to format dates used in files and chat timestamps to a humanized format. -//Mostly I wanted this to be for file names, but couldn't figure out exactly where the filename save code was as everything seemed to be connected. +//Mostly I wanted this to be for file names, but couldn't figure out exactly where the filename save code was as everything seemed to be connected. //During testing, this performs the same as previous date.now() structure. //It also does not break old characters/chats, as the code just uses whatever timestamp exists in the chat. //New chats made with characters will use this new formatting. @@ -145,7 +147,6 @@ const tokenizersCache = {}; function getTiktokenTokenizer(model) { if (tokenizersCache[model]) { - console.log('Using the cached tokenizer instance for', model); return tokenizersCache[model]; } @@ -897,12 +898,12 @@ async function charaWrite(img_url, data, target_img, response = undefined, mes = async function tryReadImage(img_url, crop) { try { let rawImg = await jimp.read(img_url); - + // Apply crop if defined if (typeof crop == 'object' && [crop.x, crop.y, crop.width, crop.height].every(x => typeof x === 'number')) { rawImg = rawImg.crop(crop.x, crop.y, crop.width, crop.height); } - + const image = await rawImg.cover(AVATAR_WIDTH, AVATAR_HEIGHT).getBufferAsync(jimp.MIME_PNG); return image; } @@ -913,61 +914,7 @@ async function tryReadImage(img_url, crop) { } async function charaRead(img_url, input_format) { - let format; - if (input_format === undefined) { - if (img_url.indexOf('.webp') !== -1) { - format = 'webp'; - } else { - format = 'png'; - } - } else { - format = input_format; - } - - switch (format) { - case 'webp': - try { - const exif_data = await ExifReader.load(fs.readFileSync(img_url)); - let char_data; - - if (exif_data['UserComment']['description']) { - let description = exif_data['UserComment']['description']; - if (description === 'Undefined' && exif_data['UserComment'].value && exif_data['UserComment'].value.length === 1) { - description = exif_data['UserComment'].value[0]; - } - try { - json5.parse(description); - char_data = description; - } catch { - const byteArr = description.split(",").map(Number); - const uint8Array = new Uint8Array(byteArr); - const char_data_string = utf8Decode.decode(uint8Array); - char_data = char_data_string; - } - } else { - console.log('No description found in EXIF data.'); - return false; - } - return char_data; - } - catch (err) { - console.log(err); - return false; - } - case 'png': - const buffer = fs.readFileSync(img_url); - const chunks = extract(buffer); - - const textChunks = chunks.filter(function (chunk) { - return chunk.name === 'tEXt'; - }).map(function (chunk) { - return PNGtext.decode(chunk.data); - }); - var base64DecodedData = Buffer.from(textChunks[0].text, 'base64').toString('utf8'); - return base64DecodedData;//textChunks[0].text; - default: - break; - } + return characterCardParser.parse(img_url, input_format); } app.post("/getcharacters", jsonParser, function (request, response) { @@ -1404,7 +1351,7 @@ function getImages(path) { .sort(Intl.Collator().compare); } -//***********Novel.ai API +//***********Novel.ai API app.post("/getstatus_novelai", jsonParser, function (request, response_getstatus_novel = response) { @@ -1754,6 +1701,17 @@ app.post("/exportcharacter", jsonParser, async function (request, response) { return response.sendStatus(400); }); +app.post("/importgroupchat", urlencodedParser, function (request, response) { + try { + const filedata = request.file; + const chatname = humanizedISO8601DateTime(); + fs.copyFileSync(`./uploads/${filedata.filename}`, (`${directories.groupChats}/${chatname}.jsonl`)); + return response.send({ res: chatname }); + } catch (error) { + console.error(error); + return response.send({ error: true }); + } +}); app.post("/importchat", urlencodedParser, function (request, response) { if (!request.body) return response.sendStatus(400); @@ -1763,9 +1721,8 @@ app.post("/importchat", urlencodedParser, function (request, response) { let avatar_url = (request.body.avatar_url).replace('.png', ''); let ch_name = request.body.character_name; if (filedata) { - if (format === 'json') { - fs.readFile('./uploads/' + filedata.filename, 'utf8', (err, data) => { + fs.readFile(`./uploads/${filedata.filename}`, 'utf8', (err, data) => { if (err) { console.log(err); @@ -1782,7 +1739,6 @@ app.post("/importchat", urlencodedParser, function (request, response) { user_name: 'You', character_name: ch_name, create_date: humanizedISO8601DateTime(), - }, ...history.msgs.map( (message) => ({ @@ -1803,7 +1759,7 @@ app.post("/importchat", urlencodedParser, function (request, response) { const errors = []; newChats.forEach(chat => fs.writeFile( - chatsPath + avatar_url + '/' + ch_name + ' - ' + humanizedISO8601DateTime() + ' imported.jsonl', + `${chatsPath + avatar_url}/${ch_name} - ${humanizedISO8601DateTime()} imported.jsonl`, chat.map(JSON.stringify).join('\n'), 'utf8', (err) => err ?? errors.push(err) @@ -1832,8 +1788,7 @@ app.post("/importchat", urlencodedParser, function (request, response) { let jsonData = json5.parse(line); if (jsonData.user_name !== undefined || jsonData.name !== undefined) { - //console.log(humanizedISO8601DateTime()+':/importchat copying chat as '+ch_name+' - '+humanizedISO8601DateTime()+'.jsonl'); - fs.copyFile('./uploads/' + filedata.filename, chatsPath + avatar_url + '/' + ch_name + ' - ' + humanizedISO8601DateTime() + '.jsonl', (err) => { //added character name and replaced Date.now() with humanizedISO8601DateTime + fs.copyFile(`./uploads/${filedata.filename}`, (`${chatsPath + avatar_url}/${ch_name} - ${humanizedISO8601DateTime()}.jsonl`), (err) => { if (err) { response.send({ error: true }); return console.log(err); @@ -1849,9 +1804,7 @@ app.post("/importchat", urlencodedParser, function (request, response) { rl.close(); }); } - } - }); app.post('/importworldinfo', urlencodedParser, (request, response) => { @@ -1919,7 +1872,7 @@ app.post('/uploaduseravatar', urlencodedParser, async (request, response) => { const crop = tryParse(request.query.crop); let rawImg = await jimp.read(pathToUpload); - if (typeof crop == 'object') { + if (typeof crop == 'object' && [crop.x, crop.y, crop.width, crop.height].every(x => typeof x === 'number')) { rawImg = rawImg.crop(crop.x, crop.y, crop.width, crop.height); } @@ -2746,8 +2699,12 @@ const setupTasks = async function () { } if (listen && !config.whitelistMode && !config.basicAuthMode) { - console.error('Your SillyTavern is currently unsecurely open to the public. Enable whitelisting or basic authentication.'); - process.exit(1); + if (config.securityOverride) + console.warn("Security has been override. If it's not a trusted network, change the settings."); + else { + console.error('Your SillyTavern is currently unsecurely open to the public. Enable whitelisting or basic authentication.'); + process.exit(1); + } } if (true === cliArguments.ssl) @@ -2964,53 +2921,76 @@ app.post('/horde_models', jsonParser, async (_, response) => { response.send(models); }); +app.post('/horde_userinfo', jsonParser, async (_, response) => { + const api_key_horde = readSecret(SECRET_KEYS.HORDE); + + if (!api_key_horde) { + return response.send({ anonymous: true }); + } + + try { + const user = await ai_horde.findUser({ token: api_key_horde }); + return response.send(user); + } catch (error) { + console.error(error); + return response.sendStatus(500); + } +}) + app.post('/horde_generateimage', jsonParser, async (request, response) => { const MAX_ATTEMPTS = 100; const CHECK_INTERVAL = 3000; const api_key_horde = readSecret(SECRET_KEYS.HORDE) || ANONYMOUS_KEY; console.log('Stable Horde request:', request.body); - const generation = await ai_horde.postAsyncImageGenerate( - { - prompt: `${request.body.prompt_prefix} ${request.body.prompt} ### ${request.body.negative_prompt}`, - params: + + try { + const generation = await ai_horde.postAsyncImageGenerate( { - sampler_name: request.body.sampler, - hires_fix: request.body.enable_hr, - use_gfpgan: request.body.restore_faces, - cfg_scale: request.body.scale, - steps: request.body.steps, - width: request.body.width, - height: request.body.height, - n: 1, + prompt: `${request.body.prompt_prefix} ${request.body.prompt} ### ${request.body.negative_prompt}`, + params: + { + sampler_name: request.body.sampler, + hires_fix: request.body.enable_hr, + use_gfpgan: request.body.restore_faces, + cfg_scale: request.body.scale, + steps: request.body.steps, + width: request.body.width, + height: request.body.height, + karras: Boolean(request.body.karras), + n: 1, + }, + r2: false, + nsfw: request.body.nfsw, + models: [request.body.model], }, - r2: false, - nsfw: request.body.nfsw, - models: [request.body.model], - }, - { token: api_key_horde }); + { token: api_key_horde }); - for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) { - await delay(CHECK_INTERVAL); - const check = await ai_horde.getImageGenerationCheck(generation.id); - console.log(check); - - if (check.done) { - const result = await ai_horde.getImageGenerationStatus(generation.id); - return response.send(result.generations[0].img); + for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) { + await delay(CHECK_INTERVAL); + const check = await ai_horde.getImageGenerationCheck(generation.id); + console.log(check); + + if (check.done) { + const result = await ai_horde.getImageGenerationStatus(generation.id); + return response.send(result.generations[0].img); + } + + /* + if (!check.is_possible) { + return response.sendStatus(503); + } + */ + + if (check.faulted) { + return response.sendStatus(500); + } } - /* - if (!check.is_possible) { - return response.sendStatus(503); - } - */ - - if (check.faulted) { - return response.sendStatus(500); - } + return response.sendStatus(504); + } catch (error) { + console.error(error); + return response.sendStatus(500); } - - return response.sendStatus(504); }); function writeSecret(key, value) { @@ -3033,4 +3013,4 @@ function readSecret(key) { const fileContents = fs.readFileSync(SECRETS_FILE); const secrets = JSON.parse(fileContents); return secrets[key]; -} \ No newline at end of file +} diff --git a/src/character-card-parser.js b/src/character-card-parser.js new file mode 100644 index 000000000..e97a66bf0 --- /dev/null +++ b/src/character-card-parser.js @@ -0,0 +1,72 @@ +const fs = require('fs'); +const json5 = require('json5'); +const ExifReader = require('exifreader'); + +const extract = require('png-chunks-extract'); +const PNGtext = require('png-chunk-text'); + +const utf8Decode = new TextDecoder('utf-8', { ignoreBOM: true }); + +const parse = async (cardUrl, format) => { + let fileFormat; + if (format === undefined) { + if (cardUrl.indexOf('.webp') !== -1) + fileFormat = 'webp'; + else + fileFormat = 'png'; + } + else + fileFormat = format; + + switch (fileFormat) { + case 'webp': + try { + const exif_data = await ExifReader.load(fs.readFileSync(cardUrl)); + let char_data; + + if (exif_data['UserComment']['description']) { + let description = exif_data['UserComment']['description']; + if (description === 'Undefined' && exif_data['UserComment'].value && exif_data['UserComment'].value.length === 1) { + description = exif_data['UserComment'].value[0]; + } + + try { + json5.parse(description); + char_data = description; + } catch { + const byteArr = description.split(",").map(Number); + const uint8Array = new Uint8Array(byteArr); + const char_data_string = utf8Decode.decode(uint8Array); + char_data = char_data_string; + } + } + else { + console.log('No description found in EXIF data.'); + return false; + } + + return char_data; + } + catch (err) { + console.log(err); + return false; + } + case 'png': + const buffer = fs.readFileSync(cardUrl); + const chunks = extract(buffer); + + const textChunks = chunks.filter(function (chunk) { + return chunk.name === 'tEXt'; + }).map(function (chunk) { + return PNGtext.decode(chunk.data); + }); + + return Buffer.from(textChunks[0].text, 'base64').toString('utf8'); + default: + break; + } +}; + +module.exports = { + parse: parse +};