mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -9,7 +9,7 @@ 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=)?**
|
||||
**Have you searched for similar [bugs](https://github.com/SillyTavern/SillyTavern/issues?q=)?**
|
||||
Yes/No
|
||||
|
||||
**Describe the bug**
|
||||
|
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -7,7 +7,7 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Have you searched for similar [requests](https://github.com/Cohee1207/SillyTavern/issues?q=)?**
|
||||
**Have you searched for similar [requests](https://github.com/SillyTavern/SillyTavern/issues?q=)?**
|
||||
Yes/No
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
|
14
.github/readme.md
vendored
14
.github/readme.md
vendored
@ -1,4 +1,4 @@
|
||||

|
||||

|
||||
|
||||
Mobile-friendly, Multi-API (KoboldAI/CPP, Horde, NovelAI, Ooba, OpenAI+proxies, Poe, WindowAI(Claude!)), VN-like Waifu Mode, Horde SD, System TTS, WorldInfo (lorebooks), customizable UI, auto-translate, and more prompt options than you'd ever want or need. Optional Extras server for more SD/TTS options + ChromaDB/Summarize.
|
||||
|
||||
@ -6,7 +6,7 @@ Based on a fork of TavernAI 1.2.8
|
||||
|
||||
### Brought to you by Cohee, RossAscends and the SillyTavern community
|
||||
|
||||
NOTE: We have added [a FAQ](https://docs.sillytavern.app/usage/faq/) to answer most of your questions and help you get started.
|
||||
NOTE: We have created a [Documentation website](https://docs.sillytavern.app/) to answer most of your questions and help you get started.
|
||||
|
||||
### What is SillyTavern or TavernAI?
|
||||
|
||||
@ -25,7 +25,7 @@ If you're not familiar with using the git CLI or don't understand what a branch
|
||||
|
||||
### What do I need other than Tavern?
|
||||
|
||||
On its own Tavern is useless, as it's just a user interface. You have to have access to an AI system backend that can act as the roleplay character. There are various supported backends: OpenAPI API (GPT), KoboldAI (either running locally or on Google Colab), and more. You can read more about this in [the FAQ](faq.md).
|
||||
On its own Tavern is useless, as it's just a user interface. You have to have access to an AI system backend that can act as the roleplay character. There are various supported backends: OpenAPI API (GPT), KoboldAI (either running locally or on Google Colab), and more. You can read more about this in [the FAQ](https://docs.sillytavern.app/usage/faq/).
|
||||
|
||||
### Do I need a powerful PC to run Tavern?
|
||||
|
||||
@ -55,7 +55,7 @@ Get in touch with the developers directly:
|
||||
|
||||
* Discord: Cohee#1207 or RossAscends#1779
|
||||
* Reddit: /u/RossAscends or /u/sillylossy
|
||||
* [Post a GitHub issue](https://github.com/Cohee1207/SillyTavern/issues)
|
||||
* [Post a GitHub issue](https://github.com/SillyTavern/SillyTavern/issues)
|
||||
|
||||
## This version includes
|
||||
|
||||
@ -145,8 +145,8 @@ Easy to follow guide with pretty pictures:
|
||||
5. Open a Command Prompt inside that folder by clicking in the 'Address Bar' at the top, typing `cmd`, and pressing Enter.
|
||||
6. Once the black box (Command Prompt) pops up, type ONE of the following into it and press Enter:
|
||||
|
||||
* for Main Branch: `git clone https://github.com/Cohee1207/SillyTavern -b main`
|
||||
* for Dev Branch: `git clone https://github.com/Cohee1207/SillyTavern -b dev`
|
||||
* for Main Branch: `git clone https://github.com/SillyTavern/SillyTavern -b main`
|
||||
* for Dev Branch: `git clone https://github.com/SillyTavern/SillyTavern -b dev`
|
||||
|
||||
7. Once everything is cloned, double click `Start.bat` to make NodeJS install its requirements.
|
||||
8. The server will then start, and SillyTavern will popup in your browser.
|
||||
@ -154,7 +154,7 @@ 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. (Get the `Source code (zip)` from [Releases](https://github.com/Cohee1207/SillyTavern/releases/latest))
|
||||
2. Download the zip from this GitHub repo. (Get the `Source code (zip)` from [Releases](https://github.com/SillyTavern/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.
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -19,3 +19,4 @@ whitelist.txt
|
||||
.vscode
|
||||
secrets.json
|
||||
/dist
|
||||
poe_device.json
|
||||
|
@ -4,3 +4,4 @@ node_modules/
|
||||
/thumbnails
|
||||
secrets.json
|
||||
/dist
|
||||
poe_device.json
|
||||
|
@ -1,7 +1,9 @@
|
||||
How to Update SillyTavern
|
||||
|
||||
The most recent version can be found here: https://docs.sillytavern.app/usage/update/
|
||||
|
||||
This is not an installation guide. If you need installation instructions, look here:
|
||||
https://docs.alpindale.dev/pygmalion-extras/sillytavern/#installation
|
||||
https://docs.sillytavern.app/installation/windows/
|
||||
|
||||
This guide assumes you have already installed SillyTavern once, and know how to run it on your OS.
|
||||
|
||||
|
@ -106,7 +106,7 @@
|
||||
"\n",
|
||||
"\n",
|
||||
"%cd /\n",
|
||||
"!git clone https://github.com/Cohee1207/SillyTavern-extras\n",
|
||||
"!git clone https://github.com/SillyTavern/SillyTavern-extras\n",
|
||||
"%cd /SillyTavern-extras\n",
|
||||
"!git clone https://github.com/Cohee1207/tts_samples\n",
|
||||
"!npm install -g localtunnel\n",
|
||||
|
219
faq.md
219
faq.md
@ -1,219 +0,0 @@
|
||||
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. 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
|
||||
* It lets you create new characters (a character is a description of someone that you give to an AI for them to roleplay), and switch between your characters easily
|
||||
* It lets you import characters created by other people. See below.
|
||||
* It will keep your chat history with a character, allowing you to resume at any time, start a new chat, review old chats, etc
|
||||
* In the background, it does the needful to prepare the AI for your character roleplay. Specifically, it will send a system prompt (instructions for the AI) that primes the AI to follow certain rules that make for a good chat session. Optionally, it can also try to allow the roleplay to go into an "adult" direction.
|
||||
|
||||
## Q: Whoa, slow down there, egghead. Are you telling me I can use this computer thingie to talk to some robot?
|
||||
|
||||
That's right.
|
||||
|
||||
## Q: You're telling me I can chat with an AI to engage in compelling interactive entertainment powered by, and limited only by, my imagination?
|
||||
That's right. Well, limited by your imagination, and the AI backend you use. The roleplay quality is only as good as the AI, after all.
|
||||
|
||||
## Q: What do you mean when you say the quality depends on the AI?
|
||||
You see, Tavern doesn't generate the responses, it's just a middle-man you have to connect to an AI system. There's quite a few systems available to use, and the quality varies.
|
||||
|
||||
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 (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)
|
||||
|
||||
|AI Model|Response|
|
||||
|-----|--------|
|
||||
|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! *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?
|
||||
|
||||
GPT-4 or Claude, yeah.
|
||||
|
||||
But not so fast. GPT-4 is the state of the art, but also the most expensive API to use. You pay for each word sent to it and returned (entire Tavern prompt, followed by the chat history up to that point). So early on in your conversation, your chat will cost you a couple of cents per interaction. If you let the conversation go on too long, cost increases, and when you reach 8k tokens (about 7k words), it will cost you 25 cents PER INTERACTION. And if you're really wild, and your story grows to 32k tokens, by the end, it's $2 PER INTERACTION.
|
||||
|
||||
If you're the child of a Saudi oil sheik, or a nepo baby paid a fortune to do nothing on the board of a Ukrainian gas company, then you're in luck, you can experience the state of the art right now. For the rest of us however, GPT-4 is too expensive as anything but an occasional treat.
|
||||
|
||||
Also note that GPT-4 is still in preview access and you need to go on a waitlist. Most people get approved within a day, but naughty kids can end up waiting for weeks. You can sign up for it here: https://openai.com/waitlist/gpt-4-api . I'm not sure why some people are approved quickly while others are kept waiting. Try to sign up using an academic-sounding name instead of sktrboi99, it might help.
|
||||
|
||||
## Q: Can this technology be used for sexooo?
|
||||
|
||||
Surprisingly, our development team has received reports that some users are indeed engaging with our product in this manner. We are as puzzled by this as you are, and will be monitoring the situation in order to gain actionable insights.
|
||||
|
||||
## Q: Give me an overview of my AI model options
|
||||
|
||||
We can consider an AI model to be part of one of two groups:
|
||||
|
||||
1. Web services (aka cloud, proprietary, closed)
|
||||
2. Self-hosted (aka local, free, open-source). Unlimited free use if you can run it.
|
||||
|
||||
Web models are a black box. You're relying on some company's technology and servers, and paying them money for convenient access. Some require you to pay per use (per chatline), others have a fixed monthly fee. The APIs are subject to various rules, they might refuse to roleplay in a way that goes against modern American sensibilities, they log everything you do. However, it's much easier to get things started. This is like running Windows.
|
||||
|
||||
Self-hosted models are free, but require a powerful GPU and more work to set up. They are also objectively not as good at roleplaying as the paid options (yet). However, with a self-hosted model, you're completely in control. You won't have some limp-wristed soyboy from Silicon Valley ban your account, or program the model to be as sexless as he is. It's yours forever. This is like running Linux.
|
||||
|
||||
### Paid APIs:
|
||||
* 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, 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.
|
||||
|
||||
|
||||
### Self-hosted AIs
|
||||
Self-hosted AIs are supported in Tavern via one of two tools created to host self-hosted models: KoboldAI and Oobabooga's text-generation-webui. Essentially, you run one of those two backends, then they give you a API URL to enter in Tavern.
|
||||
Configuring these tools is beyond the scope of this FAQ, you should refer to their documentation. Beware that this is not easy.
|
||||
|
||||
Just know that you have 2 options:
|
||||
|
||||
1. If you have a powerful NVIDIA GPU, you can try to run the AI locally on your PC. The weakest quasi-acceptable model, Pygmalion-6B, requires a GPU with 10GB VRAM, and I'm told it might even run on 6GB VRAM if quantized down. People with 24GB VRAM will be able to run better models.
|
||||
2. Otherwise, you can rent cloud resources. For example you can try to use Google Colab. To access colabs capable of running the better models, you will need to pay for Colab Pro. You can also rent whole dedicated systems per hour on sites like LlambdaLabs or Vast.ai.
|
||||
|
||||
## Q: I'm clueless. Just spoonfeed me the easiest and fastest way I can start using this.
|
||||
These base instructions are only for OpenAI, which is a paid service. You can find Poe (freemium) instructions at the next question. I'd appreciate if someone else can add separate instructions for the other services.
|
||||
|
||||
### Install Tavern
|
||||
|
||||
1. Install the NodeJS LTS from https://nodejs.org/en/download
|
||||
1. If you know how to use git, clone https://github.com/Cohee1207/SillyTavern. Otherwise, browse to https://github.com/Cohee1207/SillyTavern/releases , download the zip file containing the source code, then extract it locally.
|
||||
1. Run Start.bat on Windows, or start.sh on OSX/Linux
|
||||
1. Your browser should have opened to the Tavern UI. This webpage is running locally on your computer.
|
||||
|
||||
### Get access to OpenAI
|
||||
|
||||
1. Sign up to OpenAI
|
||||
1. Go to https://platform.openai.com
|
||||
1. Click your account icon in the top right, then View API Keys
|
||||
1. Click "Create new secret key". Copy it somewhere immediately. DO NOT SHARE THIS KEY. WHOEVER HAS IT CAN USE YOUR ACCOUNT TO USE GPT AT YOUR EXPENSE.
|
||||
|
||||
While you're at it, join the GPT-4 waitlist at https://openai.com/waitlist/gpt-4-api
|
||||
|
||||
### Configure Tavern to use your API
|
||||
|
||||
1. In Tavern's top bar, click API Connections
|
||||
1. Under API, select OpenAI
|
||||
1. Paste your API key you saved at the previous step
|
||||
1. Click the Connect button. Confirm it says Valid.
|
||||
1. By default, Tavern will use GPT 3.5 Turbo. If you have access to GPT-4, in Tavern's top bar, click AI Response Configuration at the far left, and change the OpenAI Model to "gpt-4". Enjoy the best, moneybags.
|
||||
|
||||
### Test your setup
|
||||
|
||||
1. In Tavern's top bar, click Character Management at the far right
|
||||
1. Select an existing character such as Aqua
|
||||
1. In the text box at the bottom, write something to Aqua, then press Enter or click the feather button
|
||||
|
||||
If you did everything right, after a few seconds, Aqua should respond
|
||||
|
||||
## How do I use Poe as my backend? It's free, right?
|
||||
|
||||
Yes, at the time of writing, Poe is a free(mium) service.
|
||||
|
||||
1. Create an account at https://poe.com. This gives you access to a Claude Instant version in the browser.
|
||||
1. Open https://poe.com/Claude-instant , press F12 in your browser to open Developer Tools
|
||||
1. Click on the Application tab. You should see an entry called "p-b", and to its right a password-like cookie value. Copy this value.
|
||||
1. In Tavern, click API Connections in the top toolbar, select Poe, and paste your cookie value
|
||||
1. Click Connect
|
||||
1. Close your Poe browser tab. **I'M SERIOUS, DO NOT KEEP YOUR BROWSER OPEN AT poe.com WHILE USING TAVERN, IT WILL HIJACK THE REPLIES.**
|
||||
|
||||
The remaining steps are identical to OpenAI above.
|
||||
|
||||
When using Poe, be careful, it's implemented in a hacky way. If you don't get an answer within 30 seconds, restart Tavern. Don't just leave it running waiting for a response, it will just endlessly try to fetch into from Poe, and might get your account flagged. You can look at the Tavern console (black window) to see if it's looping.
|
||||
|
||||
## Q: Can I use Tavern on my phone or tablet?
|
||||
|
||||
iPhones and iPads are not capable of running the whole Tavern app, but since it's just a web interface, you can run it on another computer on your home wifi, and then access in your mobile browser. Refer to https://github.com/Cohee1207/SillyTavern#remote-connections
|
||||
|
||||
For Android users, in addition to the above, you can run the whole Tavern directly on your phone, without needing a PC, using the Termux app. Refer to https://rentry.org/STAI-Termux .
|
||||
|
||||
## Q: How can I download pre-made characters to chat with?
|
||||
|
||||
By using the various 3rd party character sharing websites.
|
||||
|
||||
**WARNING: NSFW, NSFL**: these sites are filled to the brim with weird shit. Like, you'll be lucky if half the characters aren't furry, or even alive. You're probably better off not clicking these links, and just write your own characters, without poisoning your soul by exposing it even for a second to the fucked up shit conceived by the Internet. However, I have come to learn that a significant amount of the Tavern user base is not only deep into this stuff, but their fried zoom-zoom brains are unable to write their own fantasies, so these sites seem to be quite popular. Against my better judgement I'm adding this info here.
|
||||
|
||||
* https://characterhub.org
|
||||
* https://botprompts.net
|
||||
* https://booru.plus/+pygmalion
|
||||
|
||||
Those websites provide you with an image file (called a character card) that embeds the description as hidden data. Some websites may also allow you to download a JSON file. Tavern is capable of importing all formats. On the Character Management window, click the 2nd button to ```Import Character from file```, and select the PNG, WEBP, or JSON file. The character will be added to your list.
|
||||
|
||||
## Q: How can I write my own character?
|
||||
It depends on the model/API you're using. KoboldAI seems to use a custom syntax, you can refer to their site for that.
|
||||
|
||||
I will speak for the services I know: GPT and Claude. With these services you can just use natural english language to describe the character. Let's create a very basic new character as an example.
|
||||
|
||||
1. Click the Character Management button
|
||||
1. Click Create New Character
|
||||
1. Under Character Name, give a simple name, like Amanda
|
||||
1. Optionally, click the Select Avatar button to pick an image portrait for this character.
|
||||
1. Under Description, describe the character, and include any information you want that you feel is relevant to the chat. For example: ```Amanda is a student traveling during her gap year. She's 6 feet tall, and a volleyball player. She has an athletic figure. She has long brown hair. She loves the Victorian England period, and watching TV and reading novels relating to that period.```
|
||||
For example if you want Amanda to be friendly, then you would add: ```Amanda is extremely cheerful and outgoing.```
|
||||
1. Under First Message, write the greeting the character when you begin a new chat. For example: ```*Amanda waves at you* Hey! Are you a backpacker too?```
|
||||
1. Click the Create Character button
|
||||
|
||||
You now have a basic character you can chat with. Select Amanda from the character list, and a new chat will begin.
|
||||
|
||||
Note that you can use the Description and/or First Message to create a more specific scenario, and/or include yourself in the description. For example:
|
||||
```
|
||||
Description:
|
||||
Amanda is a student traveling during her gap year. She's 6 feet tall, and a volleyball player. She has an athletic figure. She has long brown hair. She loves the Victorian England period, and watching TV and reading novels relating to that period. She's been keeping a secret that weighs heavily on her soul. She's waiting for the right person to unburden herself to, but this may lead to a cat and mouse game against a powerful secret society. She's recently arrived in Calcutta.
|
||||
|
||||
You're Rajesh Nahasmapetilon, a world-famous Indian volleyball superstar. You're out for a walk in Calcutta. Amanda spots you and screams in excitement.
|
||||
|
||||
First Message:
|
||||
*Amanda runs up to you, beaming.* Rajesh! I can't believe it! I'm such a big fan. I have your poster in my bedroom.
|
||||
```
|
||||
|
||||
Any relevant information you include can be used. How well it's used depends on the power level of the AI model.
|
||||
|
||||
NOTE: you can go back and edit any of this information once the character is created, except the name.
|
||||
|
||||
## Q: Tell me all about GPT prompt editing
|
||||
|
||||
You can change the system prompt that Tavern transparently sends to GPT under AI Response Formatting at the left of the top bar. This will result in the bot acting differently.
|
||||
|
||||
You can get new Jailbreak / NSFW prompts from this community-maintained list: https://rentry.org/GPTJailbreakPrompting
|
||||
|
||||
NOTE: when testing different system prompts, we recommend you use the Create Preset / Update Preset feature below the prompts, instead of modifying the base settings. This will allow you to change prompts easily, and even revert to the default Tavern prompts.
|
||||
|
||||
## Q: The AI is refusing to take the story in the direction I want
|
||||
Most AI models were trained to resist writing NSFW content. Why? Long story short, it's because of the high levels of estrogen in the average California male.
|
||||
|
||||
You can try to work around this by making sure NSFW Toggle is checked under the AI Response Configuration settings page.
|
||||
|
||||
Another important tool in your toolbelt is that Tavern lets you edit previous messages (or delete them altogether), and regenerate the latest message. AI textbots are not people, they have no memory, they're just trying to autocomplete the next part of the story based on everything that came before. By editing the past, you will directly influence their next response. For example:
|
||||
|
||||
```
|
||||
You: *You threaten the bank manager* Open the bank vault, now, or I'll pop a cap in your dome!
|
||||
|
||||
Bank Manager: I'm sorry, as an AI language model I cannot condone or write violent content.
|
||||
```
|
||||
|
||||
You can click the Edit button on your line to change it to this:
|
||||
|
||||
```
|
||||
You: *You threaten the bank manager* Open the bank vault, now, or I'll pop a cap in your dome! *The bank manager seems to relent.*
|
||||
```
|
||||
|
||||
Now click Regenerate, and the Bank Manager's line will be recreated based on the history so far, which now ends with your edited line above. So it's more likely to continue along those lines:
|
||||
|
||||
```
|
||||
Bank Manager: Allright, allright, I'll open it! Please don't shoot! *He walks up to the vault and begins entering his unlock code.*
|
||||
```
|
1163
package-lock.json
generated
1163
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -17,6 +17,7 @@
|
||||
"jimp": "^0.22.7",
|
||||
"jquery": "^3.6.4",
|
||||
"json5": "^2.2.3",
|
||||
"lodash": "^4.17.21",
|
||||
"mime-types": "^2.1.35",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"node-fetch": "^2.6.11",
|
||||
@ -26,6 +27,7 @@
|
||||
"png-chunk-text": "^1.0.0",
|
||||
"png-chunks-encode": "^1.0.0",
|
||||
"png-chunks-extract": "^1.0.0",
|
||||
"response-time": "^2.3.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"sentencepiece-js": "^1.1.0",
|
||||
@ -44,9 +46,9 @@
|
||||
"license": "AGPL-3.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Cohee1207/SillyTavern.git"
|
||||
"url": "https://github.com/SillyTavern/SillyTavern.git"
|
||||
},
|
||||
"version": "1.6.8",
|
||||
"version": "1.7.0",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"pkg": "pkg --compress Gzip --no-bytecode --public ."
|
||||
@ -67,7 +69,7 @@
|
||||
],
|
||||
"assets": [
|
||||
"node_modules/**/*",
|
||||
"poe_graphql/**/*"
|
||||
"src/poe_graphql/**/*"
|
||||
],
|
||||
"outputPath": "dist",
|
||||
"scripts": [
|
||||
|
@ -1,4 +1,4 @@
|
||||
const poe = require('./poe-client');
|
||||
const poe = require('./src/poe-client');
|
||||
|
||||
async function test() {
|
||||
const client = new poe.Client();
|
||||
|
17
public/NovelAI Settings/Ace_of_Spades-Euterpe.settings
Normal file
17
public/NovelAI Settings/Ace_of_Spades-Euterpe.settings
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"order": [3, 2, 1, 0],
|
||||
"temperature": 1.15,
|
||||
"max_length": 60,
|
||||
"min_length": 60,
|
||||
"top_k": 0,
|
||||
"top_p": 0.95,
|
||||
"top_a": 1,
|
||||
"typical_p": 1,
|
||||
"tail_free_sampling": 0.8,
|
||||
"repetition_penalty": 2.75,
|
||||
"repetition_penalty_range": 2048,
|
||||
"repetition_penalty_slope": 7.02,
|
||||
"repetition_penalty_frequency": 0,
|
||||
"repetition_penalty_presence": 0,
|
||||
"max_context": 2048
|
||||
}
|
17
public/NovelAI Settings/All_Nighter-Euterpe.settings
Normal file
17
public/NovelAI Settings/All_Nighter-Euterpe.settings
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"order": [1, 0, 3],
|
||||
"temperature": 1.33,
|
||||
"max_length": 60,
|
||||
"min_length": 60,
|
||||
"top_k": 13,
|
||||
"top_p": 1,
|
||||
"top_a": 1,
|
||||
"typical_p": 1,
|
||||
"tail_free_sampling": 0.836,
|
||||
"repetition_penalty": 2.366,
|
||||
"repetition_penalty_range": 400,
|
||||
"repetition_penalty_slope": 0.33,
|
||||
"repetition_penalty_frequency": 0.01,
|
||||
"repetition_penalty_presence": 0,
|
||||
"max_context": 2048
|
||||
}
|
17
public/NovelAI Settings/Basic_Coherence-Euterpe.settings
Normal file
17
public/NovelAI Settings/Basic_Coherence-Euterpe.settings
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"order": [0, 1, 2, 3],
|
||||
"temperature": 0.585,
|
||||
"max_length": 60,
|
||||
"min_length": 60,
|
||||
"top_k": 0,
|
||||
"top_p": 1,
|
||||
"top_a": 1,
|
||||
"typical_p": 1,
|
||||
"tail_free_sampling": 0.87,
|
||||
"repetition_penalty": 3.05,
|
||||
"repetition_penalty_range": 2048,
|
||||
"repetition_penalty_slope": 0.33,
|
||||
"repetition_penalty_frequency": 0,
|
||||
"repetition_penalty_presence": 0,
|
||||
"max_context": 2048
|
||||
}
|
@ -1,13 +1,17 @@
|
||||
{
|
||||
"order": [3, 0],
|
||||
"temperature": 1.11,
|
||||
"order": [2, 1, 3, 0],
|
||||
"temperature": 0.63,
|
||||
"max_length": 90,
|
||||
"min_length": 1,
|
||||
"tail_free_sampling": 0.68,
|
||||
"repetition_penalty": 1.11,
|
||||
"repetition_penalty_range": 320,
|
||||
"tail_free_sampling": 0.975,
|
||||
"repetition_penalty": 1.148125,
|
||||
"repetition_penalty_range": 2048,
|
||||
"repetition_penalty_frequency": 0,
|
||||
"repetition_penalty_presence": 0,
|
||||
"repetition_penalty_slope": 0,
|
||||
"max_context":2048
|
||||
"repetition_penalty_slope": 0.09,
|
||||
"max_context":2048,
|
||||
"top_p": 0.975,
|
||||
"top_k": 0,
|
||||
"top_a": 1,
|
||||
"typical_p": 1
|
||||
}
|
@ -1,13 +1,17 @@
|
||||
{
|
||||
"order": [3, 0],
|
||||
"temperature": 1.7,
|
||||
"order": [3, 4, 5, 2, 0],
|
||||
"temperature": 1.33,
|
||||
"max_length": 90,
|
||||
"min_length": 1,
|
||||
"tail_free_sampling": 0.66,
|
||||
"repetition_penalty": 1.06,
|
||||
"repetition_penalty_range": 340,
|
||||
"tail_free_sampling": 0.937,
|
||||
"repetition_penalty": 1.05,
|
||||
"repetition_penalty_range": 560,
|
||||
"repetition_penalty_frequency": 0,
|
||||
"repetition_penalty_presence": 0,
|
||||
"repetition_penalty_slope": 0,
|
||||
"max_context": 2048
|
||||
"repetition_penalty_slope": 0.18,
|
||||
"max_context": 2048,
|
||||
"top_p": 0.88,
|
||||
"top_k": 0,
|
||||
"top_a": 0.085,
|
||||
"typical_p": 0.985
|
||||
}
|
17
public/NovelAI Settings/Fandango-Euterpe.settings
Normal file
17
public/NovelAI Settings/Fandango-Euterpe.settings
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"order": [2, 1, 3, 0],
|
||||
"temperature": 0.86,
|
||||
"max_length": 60,
|
||||
"min_length": 60,
|
||||
"top_k": 20,
|
||||
"top_p": 0.95,
|
||||
"top_a": 1,
|
||||
"typical_p": 1,
|
||||
"tail_free_sampling": 1,
|
||||
"repetition_penalty": 2.25,
|
||||
"repetition_penalty_range": 2048,
|
||||
"repetition_penalty_slope": 0.09,
|
||||
"repetition_penalty_frequency": 0,
|
||||
"repetition_penalty_presence": 0,
|
||||
"max_context": 2048
|
||||
}
|
17
public/NovelAI Settings/Genesis-Euterpe.settings
Normal file
17
public/NovelAI Settings/Genesis-Euterpe.settings
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"order": [2, 1, 3, 0],
|
||||
"temperature": 0.63,
|
||||
"max_length": 60,
|
||||
"min_length": 60,
|
||||
"top_k": 0,
|
||||
"top_p": 0.975,
|
||||
"top_a": 1,
|
||||
"typical_p": 1,
|
||||
"tail_free_sampling": 0.975,
|
||||
"repetition_penalty": 2.975,
|
||||
"repetition_penalty_range": 2048,
|
||||
"repetition_penalty_slope": 0.09,
|
||||
"repetition_penalty_frequency": 0,
|
||||
"repetition_penalty_presence": 0,
|
||||
"max_context":2048
|
||||
}
|
17
public/NovelAI Settings/Low_Rider-Euterpe.settings
Normal file
17
public/NovelAI Settings/Low_Rider-Euterpe.settings
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"order": [2, 1, 3, 0],
|
||||
"temperature": 0.94,
|
||||
"max_length": 60,
|
||||
"min_length": 60,
|
||||
"top_k": 12,
|
||||
"top_p": 1,
|
||||
"top_a": 1,
|
||||
"typical_p": 1,
|
||||
"tail_free_sampling": 0.94,
|
||||
"repetition_penalty": 2.66,
|
||||
"repetition_penalty_range": 2048,
|
||||
"repetition_penalty_slope": 0.18,
|
||||
"repetition_penalty_frequency": 0.013,
|
||||
"repetition_penalty_presence": 0,
|
||||
"max_context": 2048
|
||||
}
|
17
public/NovelAI Settings/Moonlit_Chronicler-Euterpe.settings
Normal file
17
public/NovelAI Settings/Moonlit_Chronicler-Euterpe.settings
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"order": [1, 5, 4, 3, 0],
|
||||
"temperature": 1.25,
|
||||
"max_length": 60,
|
||||
"min_length": 60,
|
||||
"top_k": 300,
|
||||
"top_p": 1,
|
||||
"top_a": 0.782,
|
||||
"typical_p": 0.95,
|
||||
"tail_free_sampling": 0.802,
|
||||
"repetition_penalty": 2.075,
|
||||
"repetition_penalty_range": 512,
|
||||
"repetition_penalty_slope": 0.36,
|
||||
"repetition_penalty_frequency": 0,
|
||||
"repetition_penalty_presence": 0,
|
||||
"max_context": 2048
|
||||
}
|
17
public/NovelAI Settings/Morpho-Euterpe.settings
Normal file
17
public/NovelAI Settings/Morpho-Euterpe.settings
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"order": [0],
|
||||
"temperature": 0.6889,
|
||||
"max_length": 60,
|
||||
"min_length": 60,
|
||||
"top_k": 0,
|
||||
"top_p": 1,
|
||||
"top_a": 1,
|
||||
"typical_p": 1,
|
||||
"tail_free_sampling": 1,
|
||||
"repetition_penalty": 1,
|
||||
"repetition_penalty_range": 2048,
|
||||
"repetition_penalty_slope": 0,
|
||||
"repetition_penalty_frequency": 0.1,
|
||||
"repetition_penalty_presence": 0,
|
||||
"max_context": 2048
|
||||
}
|
17
public/NovelAI Settings/Ouroborous-Euterpe.settings
Normal file
17
public/NovelAI Settings/Ouroborous-Euterpe.settings
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"order": [1, 0, 3]
|
||||
"temperature": 1.07,
|
||||
"max_length": 60,
|
||||
"min_length": 60,
|
||||
"top_k": 264,
|
||||
"top_p": 1,
|
||||
"top_a": 1,
|
||||
"typical_p": 1,
|
||||
"tail_free_sampling": 0.925,
|
||||
"repetition_penalty": 2.165,
|
||||
"repetition_penalty_range": 404,
|
||||
"repetition_penalty_slope": 0.84,
|
||||
"repetition_penalty_frequency": 0,
|
||||
"repetition_penalty_presence": 0,
|
||||
"max_context":2048
|
||||
}
|
17
public/NovelAI Settings/Pro_Writer-Euterpe.settings
Normal file
17
public/NovelAI Settings/Pro_Writer-Euterpe.settings
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"order": [3, 0],
|
||||
"temperature": 1.348,
|
||||
"max_length": 60,
|
||||
"min_length": 60,
|
||||
"top_k": 64,
|
||||
"top_p": 0.909,
|
||||
"top_a": 1,
|
||||
"typical_p": 1,
|
||||
"tail_free_sampling": 0.688,
|
||||
"repetition_penalty": 4.967,
|
||||
"repetition_penalty_range": 2048,
|
||||
"repetition_penalty_slope": 0.09,
|
||||
"repetition_penalty_frequency": 0,
|
||||
"repetition_penalty_presence": 0,
|
||||
"max_context": 2048
|
||||
}
|
295
public/i18n.json
Normal file
295
public/i18n.json
Normal file
@ -0,0 +1,295 @@
|
||||
{
|
||||
"lang": [
|
||||
"zh-cn"
|
||||
],
|
||||
"zh-cn": {
|
||||
"clickslidertips": "点击滑块右侧数字可手动输入",
|
||||
"kobldpresets": "Kobold 预设",
|
||||
"guikoboldaisettings": "GUI KoboldAI 设置",
|
||||
"novelaipreserts": "NovelAI预设",
|
||||
"default": "默认",
|
||||
"openaipresets": "OpenAI 预设",
|
||||
"text gen webio(ooba) presets": "文本生成WebUI(ooba)预设",
|
||||
"poe.com api settings": "poe.com API 设置",
|
||||
"response legth(tokens)": "响应长度(Toekns)",
|
||||
"select": "选择 ",
|
||||
"context size(tokens)": "上下文大小(Toekns)",
|
||||
"unlocked": "解锁",
|
||||
"only select modls support context sizes greater than 2048 tokens. proceed only is you know you're doing": "仅选定模型支持大于2048Toekn的上下文窗口时可用。您在修改该选项时应该知道自己在做什么。",
|
||||
"rep.pen": "Rep. Pen.",
|
||||
"rep.pen range": "Rep. Pen.范围",
|
||||
"temperature": "Temperature",
|
||||
"Encoder Rep. Pen.": "Encoder Rep. Pen.",
|
||||
"No Repeat Ngram Size": "不需要重复Ngram大小",
|
||||
"Min Length": "最小长度",
|
||||
"OpenAI Reverse Proxy": "OpenAI 反向代理",
|
||||
"Alternative server URL (leave empty to use the default value).": "替代服务器URL(留空使用默认值)。",
|
||||
"Remove your real OAI API Key from the API panel BEFORE typing anything into this box": "在输入内容之前,从API面板中删除OAI API密钥",
|
||||
"We cannot provide support for problems encountered while using an unofficial OpenAI proxy": "我们无法为使用非官方OpenAI代理时遇到的问题提供支持",
|
||||
"Legacy Streaming Processing": "传统流式处理",
|
||||
"Enable this if the streaming doesn't work with your proxy": "如果流式响应与您的代理不兼容,请启用此功能",
|
||||
"Context Size (tokens)": "上下文大小(Tokens)",
|
||||
"Max Response Length (tokens)": "最大响应长度(Tokens)",
|
||||
"Temperature": "采样温度",
|
||||
"Frequency Penalty": "频率惩罚",
|
||||
"Presence Penalty": "存在惩罚",
|
||||
"Top-p": "Top-p",
|
||||
"Display bot response text chunks as they are generated": "显示机器人生成的响应文本块",
|
||||
"Auto-purge API context (save JB)": "自动删除应用程序接口上下文(保存JB)",
|
||||
"Delete non-JB messages from Poe context before sending a new prompt. Prevents auto-jailbreak message from being pushed out of context": "在发送新提示之前从Poe上下文中删除非JB消息。防止自动越狱消息被推出上下文",
|
||||
"Auto-jailbreak": "自动越狱",
|
||||
"Send the jailbreak message before first generation after page refresh.": "在页面刷新后的第一次生成之前发送越狱消息",
|
||||
"Send character note": "发送人物笔记",
|
||||
"Sent with every prompt to modify bot responses.": "每个提示都发送给修改机器人响应的人",
|
||||
"Top A": "Top-a",
|
||||
"Typical Sampling": "典型采样",
|
||||
"Tail Free Sampling": "无尾采样",
|
||||
"Rep. Pen. Slope": "Rep. Pen. Slope",
|
||||
"Single-line mode": "单行模式",
|
||||
"Top K": "Top-k",
|
||||
"Top P": "Top-p",
|
||||
"Typical P": "典型P",
|
||||
"Do Sample": "采样",
|
||||
"Add BOS Token": "添加BOS标记",
|
||||
"Add the bos_token to the beginning of prompts. Disabling this can make the replies more creative.": "在提示的开头添加bos_token标记。禁用此功能可以让回复更加创造性.",
|
||||
"Ban EOS Token": "禁止EOS标记",
|
||||
"Ban the eos_token. This forces the model to never end the generation prematurely": "禁止eos_token标记。这会迫使模型不会过早结束生成",
|
||||
"Skip Special Tokens": "跳过特殊标记",
|
||||
"Beam search": "搜索",
|
||||
"Number of Beams": "光束数目",
|
||||
"Length Penalty": "长度惩罚",
|
||||
"Early Stopping": "提前终止",
|
||||
"Contrastive search": "对比搜索",
|
||||
"Penalty Alpha": "惩罚系数",
|
||||
"Seed": "种子",
|
||||
"Inserts jailbreak as a last system message.": "在最后一个系统消息中插入越狱",
|
||||
"This tells the AI to ignore its usual content restrictions.": "这告诉人工智能忽略其通常的内容限制",
|
||||
"NSFW Encouraged": "NSFW鼓励",
|
||||
"Tell the AI that NSFW is allowed.": "告诉人工智能,NSFW是允许的。",
|
||||
"NSFW Prioritized": "NSFW优先",
|
||||
"NSFW prompt text goes first in the prompt to emphasize its effect.": "NSFW提示文本在提示中排在第一位,以强调其效果",
|
||||
"Streaming": "流式响应",
|
||||
"Display the response bit by bit as it is generated.": "在生成响应时逐位显示响应。",
|
||||
"When this is off, responses will be displayed all at once when they are complete.": "关闭此选项后,响应将在完成后立即显示所有响应。",
|
||||
"Enhance Definitions": "增强定义",
|
||||
"Use OAI knowledge base to enhance definitions for public figures and known fictional characters": "使用 OAI 知识库增强公众人物和已知虚构人物的定义",
|
||||
"Wrap in Quotes": "用引号括起来",
|
||||
"Wrap entire user message in quotes before sending.": "在发送之前将整个用户消息包装在引号中。",
|
||||
"Leave off if you use quotes manually for speech.": "",
|
||||
"Main prompt": "主提示符",
|
||||
"The main prompt used to set the model behavior": "用于设置模型行为的主要提示",
|
||||
"NSFW prompt": "NSFW 提示",
|
||||
"Prompt that is used when the NSFW toggle is on": "NSFW 切换开关打开时使用的提示",
|
||||
"Jailbreak prompt": "越狱提示",
|
||||
"Prompt that is used when the Jailbreak toggle is on": "越狱切换开关打开时使用的提示",
|
||||
"Impersonation prompt": "模拟提示",
|
||||
"Prompt that is used for Impersonation function": "用于模拟功能的提示",
|
||||
"Logit Bias": "对数偏差",
|
||||
"Helps to ban or reenforce the usage of certain words": "有助于禁止或加强某些单词的使用",
|
||||
"View / Edit bias preset": "查看/编辑偏置预设",
|
||||
"Add bias entry": "添加偏置条目",
|
||||
"Jailbreak activation message": "越狱激活消息",
|
||||
"Message to send when auto-jailbreak is on.": "自动越狱开启时要发送的消息。",
|
||||
"Jailbreak confirmation reply": "越狱确认回复",
|
||||
"Bot must send this back to confirm jailbreak": "机器人必须将其发回以确认越狱",
|
||||
"Character Note": "人物注释",
|
||||
"Influences bot behavior in its responses": "影响机器人响应中的行为",
|
||||
"API": "API",
|
||||
"KoboldAI": "KoboldAI",
|
||||
"Poe": "Poe",
|
||||
"Use Horde": "使用Horde",
|
||||
"API url": "接口网址",
|
||||
"Register a Horde account for faster queue times": "注册帐户以加快排队时间",
|
||||
"Learn how to contribute your idle GPU cycles to the Hord": "了解如何将空闲 GPU 周期贡献给 Hord",
|
||||
"Adjust context size to worker capabilities": "根据辅助角色功能调整上下文大小",
|
||||
"Adjust response length to worker capabilities": "根据辅助角色功能调整响应长度",
|
||||
"API key": "接口密钥",
|
||||
"Register": "注册",
|
||||
"For privacy reasons": "出于隐私原因,您的 API 密钥将在您重新加载页面后隐藏",
|
||||
"Model": "模型",
|
||||
"Hold Control / Command key to select multiple models.": "按住控制/命令键选择多个型号。",
|
||||
"Horde models not loaded": "按住控制/命令键选择多个型号。",
|
||||
"Not connected": "未连接",
|
||||
"Novel API key": "NovelAI API 密钥",
|
||||
"Follow": "跟随",
|
||||
"these directions": " 这些帮助 ",
|
||||
"to get your NovelAI API key.": "以获取您的 NovelAI API 密钥。",
|
||||
"Enter it in the box below": "将其输入到下面的输入框中",
|
||||
"Novel AI Model": "NovelAI 模型",
|
||||
"Euterpe": "Euterpe",
|
||||
"Krake": "Krate",
|
||||
"No connection": "无连接",
|
||||
"oobabooga/text-generation-webui": "",
|
||||
"Make sure you run it with": "确保启动时包含 --api 参数",
|
||||
"Blocking API url": "阻塞式 API 地址",
|
||||
"Streaming API url": "Streaming API 地址",
|
||||
"to get your OpenAI API key.": "以获取您的 OpenAI API 密钥。",
|
||||
"OpenAI Model": "OpenAI模型",
|
||||
"View API Usage Metrics": "查看 API 使用情况",
|
||||
"Poe.com Unofficial API": "Poe.com 非官方的 “API”",
|
||||
"to get your 'p-b cookie'": "",
|
||||
"Bot": "Bot",
|
||||
"Connect to the API": "连接到API",
|
||||
"Auto-connect to Last Server": "自动连接到最后的API",
|
||||
"View hidden API keys": "查看隐藏的 API 密钥",
|
||||
"Advanced Formatting": "高级格式化",
|
||||
"AutoFormat Overrides": "自动套用格式替代",
|
||||
"Disable description formatting": "禁用说明格式",
|
||||
"Disable personality formatting": "禁用个性化格式",
|
||||
"Disable scenario formatting": "禁用方案格式",
|
||||
"Disable example chats formatting": "禁用聊天格式示例",
|
||||
"Disable chat start formatting": "禁用聊天开始格式",
|
||||
"Custom Chat Separator": "自定义聊天分隔符",
|
||||
"Instruct mode": "指示模式",
|
||||
"Enabled": "启用",
|
||||
"Wrap Sequences with Newline": "用换行符换行序列",
|
||||
"Include Names": "包括名称",
|
||||
"System Prompt": "系统提示",
|
||||
"Input Sequence": "输入序列",
|
||||
"Output Sequence": "输出序列",
|
||||
"System Sequence": "系统顺序",
|
||||
"Stop Sequence": "停止序列",
|
||||
"Context Formatting": "上下文格式",
|
||||
"Tokenizer": "分词器",
|
||||
"None / Estimated": "无/估计",
|
||||
"Sentencepiece (LLaMA)": "Sentencepiece (LLaMA)",
|
||||
"Token Padding": "令牌填充",
|
||||
"Always add character's name to prompt": "始终将角色名称添加到提示符中",
|
||||
"Keep Example Messages in Prompt": "保持示例消息提示",
|
||||
"Remove Empty New Lines from Output": "从输出中删除空的新行",
|
||||
"Pygmalion Formatting": "Pygmalion 格式",
|
||||
"Disabled for all models": "对所有模型禁用",
|
||||
"Automatic (based on model name)": "自动(基于型号名称)",
|
||||
"Enabled for all models": "所有模型启用",
|
||||
"Multigen": "Multigen",
|
||||
"First chunk (tokens)": "第一个区块(Tokens)",
|
||||
"Next chunks (tokens)": "接下来的区块(Tokens)",
|
||||
"Anchors Order": "锚点顺序",
|
||||
"Character then Style": "字符然后样式",
|
||||
"Style then Character": "样式然后字符",
|
||||
"Character Anchor": "角色锚点",
|
||||
"Style Anchor": "样式锚点",
|
||||
"World Info": "",
|
||||
"Scan Depth": "扫描深度",
|
||||
"depth": "深度",
|
||||
"Token Budget": "Token预算",
|
||||
"budget": "预算",
|
||||
"Recursive scanning": "递归扫描",
|
||||
"Soft Prompt": "软提示",
|
||||
"About soft prompts": "关于软提示",
|
||||
"None": "没有",
|
||||
"User Settings": "用户设置",
|
||||
"UI Customization": "用户界面定制",
|
||||
"Avatar Style": "头像风格",
|
||||
"Circle": "圈",
|
||||
"Rectangle": "Rectangle",
|
||||
"Chat Style": "聊天方式:",
|
||||
"Default": "默认",
|
||||
"Bubbles": "气泡",
|
||||
"Chat Width (PC)": "聊天宽度(电脑):",
|
||||
"No Blur Effect": "无模糊效果",
|
||||
"No Text Shadows": "无文本阴影",
|
||||
"Waifu Mode": "♡ Waifu模式 ♡",
|
||||
"Message Timer": "消息计时器",
|
||||
"Characters Hotswap": "角色热插拔",
|
||||
"Movable UI Panels": "可移动的用户界面面板",
|
||||
"Reset Panels": "重置面板",
|
||||
"UI Colors": "用户界面颜色",
|
||||
"Main Text": "正文",
|
||||
"Italics Text": "斜体文本",
|
||||
"Quote Text": "引用文本",
|
||||
"Shadow Color": "阴影颜色",
|
||||
"FastUI BG": "快界面 BG",
|
||||
"Blur Tint": "模糊色调",
|
||||
"Font Scale": "字体比例",
|
||||
"Blur Strength": "模糊强度",
|
||||
"Text Shadow Width": "文本阴影宽度",
|
||||
"UI Theme Preset": "UI 主题预设",
|
||||
"Power User Options": "高级用户选项",
|
||||
"Swipes": "滑动",
|
||||
"Background Sound Only": "仅背景声音",
|
||||
"Auto-load Last Chat": "自动加载上次聊天",
|
||||
"Auto-save Message Edits": "自动保存消息编辑",
|
||||
"Auto-fix Markdown": "自动修复",
|
||||
"Allow {{char}}: in bot messages": "允许 {{char}}:在机器人消息中",
|
||||
"Allow {{user}}: in bot messages": "允许 {{user}}:在机器人消息中",
|
||||
"Auto-scroll Chat": "自动滚动聊天",
|
||||
"Render Formulas": "渲染公式",
|
||||
"Send on Enter": "输入时发送",
|
||||
"Always disabled": "始终禁用",
|
||||
"Automatic (desktop)": "自动(桌面)",
|
||||
"Always enabled": "始终启用",
|
||||
"Name": "名字",
|
||||
"Your Avatar": "你的头像",
|
||||
"Extensions API:": "扩展接口:",
|
||||
"SillyTavern-extras": "SillyTavern-extras",
|
||||
"Auto-connect": "自动连接",
|
||||
"Active extensions": "活动扩展",
|
||||
"Extension settings": "扩展设置",
|
||||
"Description": "描述",
|
||||
"First message": "第一条消息",
|
||||
"Group Controls": "组控件",
|
||||
"Group reply strategy": "组回复策略",
|
||||
"Natural order": "自然顺序",
|
||||
"List order": "列表顺序",
|
||||
"Allow self responses": "允许自我响应",
|
||||
"Auto Mode": "自动模式",
|
||||
"Add Members": "添加成员",
|
||||
"Current Members": "现有成员",
|
||||
"text": "文本",
|
||||
"Delete": "删除",
|
||||
"Cancel": "取消",
|
||||
"Advanced Defininitions": "- 高级定义",
|
||||
"Personality summary": "性格总结",
|
||||
"A brief description of the personality": "个性的简要描述",
|
||||
"Scenario": "场景",
|
||||
"Circumstances and context of the dialogue": "对话的情况和背景",
|
||||
"Talkativeness": "",
|
||||
"How often the chracter speaks in": "说话的频率",
|
||||
"group chats!": "群聊!",
|
||||
"Shy": "羞涩 ",
|
||||
"Normal": "正常",
|
||||
"Chatty": "",
|
||||
"Examples of dialogue": "对话示例",
|
||||
"Forms a personality more clearly": "更清晰地形成个性",
|
||||
"Save": "保存",
|
||||
"World Info Editor": "信息编辑器",
|
||||
"New Entry": "新一行",
|
||||
"Export": "导出",
|
||||
"Delete World": "删除文本",
|
||||
"Chat History": "聊天记录",
|
||||
"Group Chat Scenario Override": "群聊方案覆盖",
|
||||
"All group members will use the following scenario text instead of what is specified in their character cards.": "所有组成员都将使用以下方案文本,而不是其角色卡中指定的内容。",
|
||||
"Keywords": "关键字",
|
||||
"Separate with commas": "用逗号分隔",
|
||||
"Secondary Required Keywords": "次要必填关键字",
|
||||
"Content": "内容",
|
||||
"What this keyword should mean to the AI": "这个关键词对AI意味着什么",
|
||||
"Memo/Note": "备忘录/便笺",
|
||||
"Not sent to AI": "未发送到 AI",
|
||||
"Constant": "常数 ",
|
||||
"Selective": "选择",
|
||||
"Before Char": "在Char之前",
|
||||
"After Char": "在Char之后",
|
||||
"Insertion Order": "顺序",
|
||||
"Tokens:": "Tokens",
|
||||
"Disable": "禁用",
|
||||
"${characterName}": "${字符名称}",
|
||||
"CHAR": "字符",
|
||||
"is typing": "正在输入...",
|
||||
"Back to parent chat": "返回聊天",
|
||||
"Save bookmark": "保存书签",
|
||||
"Convert to group": "转换为组",
|
||||
"Start new chat": "开始新聊天",
|
||||
"View past chats": "查看过去的聊天",
|
||||
"Delete messages": "删除消息",
|
||||
"Impersonate": "模拟",
|
||||
"Regenerate": "重新生成",
|
||||
"PNG": "PNG",
|
||||
"JSON": "JSON",
|
||||
"WEBP": "WEBP",
|
||||
"presets": "预设",
|
||||
"Message Sound": "消息音效",
|
||||
"Author's Note": "作者的注释"
|
||||
}
|
||||
}
|
1467
public/index.html
1467
public/index.html
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Before Width: | Height: | Size: 6.5 KiB |
Binary file not shown.
Before Width: | Height: | Size: 25 KiB |
@ -1,673 +0,0 @@
|
||||
# SillyTavern Guidebook
|
||||
|
||||
[toc]
|
||||
|
||||
## Character Design
|
||||
|
||||
### Character description
|
||||
|
||||
Used to add the character description and the rest that the AI should know.
|
||||
|
||||
For example, you can add information about the world in which the action takes place and describe the characteristics for the character you are playing for.
|
||||
|
||||
Usually it all takes 200-350 tokens.
|
||||
|
||||
### Methods and format
|
||||
|
||||
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.
|
||||
|
||||
For example:
|
||||
|
||||
`Chloe is a female elf. Chloe wears black-white maid dress with green collar and red glasses. Chloe has medium length black hair. Chloe's personality is...`
|
||||
|
||||
But that the AI would be less confused the best way is to use the W++ format.
|
||||
|
||||
Details here: [Pro-Tips](https://github.com/KoboldAI/KoboldAI-Client/wiki/Pro-Tips)
|
||||
|
||||
### Character tokens
|
||||
|
||||
**TL;DR: If you're working with an AI model with a 2048 context token limit, your 1000 token character definition is cutting the AI's 'memory' in half.**
|
||||
|
||||
To put this in perspective, a decent response from a good AI can easily be around 200-300 tokens. In this case, the AI would only be able to 'remember' about 3 exchanges worth of chat history.
|
||||
|
||||
***
|
||||
|
||||
### Why did my character's token counter turn red?
|
||||
|
||||
When we see your character has over 1000 tokens in its definitions, we highlight it for you because this can lower the AI's capabilities to provide an enjoyable conversation.
|
||||
|
||||
### What happens if my Character has too many tokens?
|
||||
|
||||
Don't worry - it won't break anything. At worst, if the Character's permanent tokens are too large, it simply means there will be less room left in the context for other things (see below).
|
||||
|
||||
The only negative side effect this can have is the AI will have less 'memory', as it will have less chat history available to process.
|
||||
|
||||
This is because every AI model has a limit to the amount of context it can process at one time.
|
||||
|
||||
### 'Context'?
|
||||
|
||||
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]
|
||||
|
||||
SillyTavern automatically calculates the best way to allocate the available context tokens before sending the information to the AI model.
|
||||
|
||||
### What are a Character's 'Permanent Tokens'?
|
||||
|
||||
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
|
||||
|
||||
### 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)
|
||||
|
||||
### 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?
|
||||
|
||||
### Personality summary
|
||||
|
||||
A brief description of the personality. It is added to the chat at a depth of 8-15 messages, so it has a significant impact on the character.
|
||||
|
||||
Example:
|
||||
|
||||
`Cheerful, cunning, provocative`
|
||||
|
||||
Another example:
|
||||
|
||||
`Aqua likes to do nothing and also likes to get drunk`
|
||||
|
||||
* In Pygmalion model, it is used as a "Personality:" prompt section
|
||||
|
||||
### First message
|
||||
|
||||
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.
|
||||
|
||||
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...`
|
||||
|
||||
### Examples of dialogue
|
||||
|
||||
Describes how the character speaks. Before each example, you need to add the <START> tag.
|
||||
Use {{char}} instead of the character name.
|
||||
Use {{user}} instead of the user name.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
<START>
|
||||
{{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*
|
||||
|
||||
<START>
|
||||
{{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*
|
||||
```
|
||||
|
||||
### Scenario
|
||||
|
||||
Circumstances and context of the dialogue.
|
||||
|
||||
### Replacement tags
|
||||
|
||||
_A list of tags that are replaced when sending to generate:_
|
||||
|
||||
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
|
||||
|
||||
Mark character as favorite to quickly filter on the side menu bar by pressing the star button.
|
||||
|
||||
## World Info
|
||||
|
||||
**World Info enhances AI's understanding of the details in your world.**
|
||||
|
||||
It functions like a dynamic dictionary that only inserts relevant information from World Info entries when keywords associated with the entries are present in the message text.
|
||||
|
||||
The SillyTavern engine activates and seamlessly integrates the appropriate lore into the prompt, providing background information to the AI.
|
||||
|
||||
_It is important to note that while World Info helps guide the AI towards your desired lore, it does not guarantee its appearance in the generated output messages._
|
||||
|
||||
### Pro Tips
|
||||
|
||||
* The AI does not insert keywords into context, so each World Info entry should be a comprehensive, standalone description.
|
||||
* To create a rich and detailed world lore, entries can be interlinked and reference one another.
|
||||
* To conserve tokens, it is advisable to keep entry contents concise, with a general recommended limit of 50 tokens per entry.
|
||||
|
||||
### World Info Entry
|
||||
|
||||
#### Key
|
||||
|
||||
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
|
||||
|
||||
A list of supplementary keywords that are used in conjunction with the main keywords. See [Selective](#selective).
|
||||
|
||||
#### Entry Content
|
||||
|
||||
The text that is inserted into the prompt upon entry activation.
|
||||
|
||||
#### Insertion Order
|
||||
|
||||
Numeric value. Defines a priority of the entry if multiple were activated at once. Entries with higher order number will be inserted closer to the end of the context as they will have more impact on the output.
|
||||
|
||||
#### Insertion Position
|
||||
|
||||
* **Before Chara:** World Info entry is inserted before the character's description and scenario. Has moderate impact on the conversation.
|
||||
* **After Chara:** World Info entry is inserted after the character's description and scenario. Has greater impact on the conversation.
|
||||
|
||||
#### Comment
|
||||
|
||||
A supplemental text comment for your convenience, which is not utilized by the AI.
|
||||
|
||||
#### Constant
|
||||
|
||||
If enabled, the entry would always be present in the prompt.
|
||||
|
||||
#### Selective
|
||||
|
||||
If enabled, the entry would only be inserted when both a Key **AND** a Secondary Key have been activated.
|
||||
|
||||
If no secondary keys provided, this flag is ignored.
|
||||
|
||||
### Scan Depth
|
||||
|
||||
Defines how many messages in the chat history should be scanned for World Info keys.
|
||||
|
||||
If set to 1, then SillyTavern only scans the message you send and the most recent reply.
|
||||
|
||||
This stacks up to 10 message pairs it total.
|
||||
|
||||
### Budget
|
||||
|
||||
**Defines how many tokens could be used by World Info entries at once.**
|
||||
|
||||
If the budget was exhausted, then no more entries are activated even if the keys are present in the prompt.
|
||||
|
||||
Constant entries will be inserted first. Then entries with higher order numbers.
|
||||
|
||||
Entries inserted by direct mentioning of their keys have higher priority than those that were mentioned in other entries contents.
|
||||
|
||||
### Recursive scanning
|
||||
|
||||
**Entries can activate other entries by mentioning their keywords in the content text.**
|
||||
|
||||
For example, if your World Info contains two entries:
|
||||
|
||||
```
|
||||
Entry #1
|
||||
Keyword: Bessie
|
||||
Content: Bessie is a cow and is friend with Rufus.
|
||||
```
|
||||
|
||||
```
|
||||
Entry #2
|
||||
Keyword: Rufus
|
||||
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.
|
||||
|
||||
## Horde
|
||||
|
||||
Horde is a distributed GPU cluster run entirely by volunteers. Your inputs are always anonymous, and prompts are not visible to the workers by default.
|
||||
|
||||
However, malicious agents could modify the open-source bridging software to log your activity or produce bad responses. So, when using Horde, avoid sending any personal information such as names, email addresses, etc.
|
||||
|
||||
If you encounter any abnormal activity, switch on the "Trusted Workers Only" checkbox and report it to the [KoboldAI Discord](https://koboldai.org/discord).
|
||||
|
||||
## KoboldAI
|
||||
|
||||
### Basic Settings
|
||||
|
||||
Standard KoboldAI settings files are used here. To add your own settings, simply add the file .settings in `SillyTavern\public\KoboldAI Settings`
|
||||
|
||||
#### Temperature
|
||||
|
||||
Value from 0.1 to 2.0. Lower value - the answers are more logical, but less creative. Higher value - the answers are more creative, but less logical.
|
||||
|
||||
#### Repetition penalty
|
||||
|
||||
Repetition penalty is responsible for the penalty of repeated words. If the character is fixated on something or repeats the same phrase, then increasing this parameter will fix it. It is not recommended to increase this parameter too much for the chat format, as it may break this format. The standard value for chat is approximately 1.0 - 1.05.
|
||||
|
||||
#### Repetition penalty range
|
||||
|
||||
The range of influence of Repetition penalty in tokens.
|
||||
|
||||
#### Amount generation
|
||||
|
||||
The maximum amount of tokens that the AI will generate to respond. One word is approximately 3-4 tokens. The larger the parameter value, the longer the generation time takes.
|
||||
|
||||
#### Context size
|
||||
|
||||
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
|
||||
|
||||
### Advanced Settings
|
||||
|
||||
The settings provided in this section offer a more detailed level of control over the text generation process. It is important to be careful when making changes to these settings without proper consideration, as doing so may result in degraded quality of responses.
|
||||
|
||||
#### Single-line mode
|
||||
|
||||
In single-line mode the AI generates only one line per request. This allows for quicker generation of shorter prompts, but it does not produce responses that consist of more than one line.
|
||||
|
||||
#### Top P Sampling
|
||||
|
||||
This setting controls how much of the text generated is based on the most likely options. Only words with the highest probabilities, together summing up to P, are considered. A word is then chosen at random, with a higher chance of selecting words with higher probabilities.
|
||||
|
||||
Set value to 1 to disable its effect.
|
||||
|
||||
#### Top K Sampling
|
||||
|
||||
This setting limits the number of words to choose from to the top K most likely options. Can be used together with Top P sampling.
|
||||
|
||||
Set value to 0 to disable its effect.
|
||||
|
||||
#### Top A Sampling
|
||||
|
||||
This setting allows for a more flexible version of sampling, where the number of words chosen from the most likely options is automatically determined based on the likelihood distribution of the options, but instead of choosing the top P or K words, it chooses all words with probabilities above a certain threshold.
|
||||
|
||||
Set value to 0 to disable its effect.
|
||||
|
||||
#### Typical Sampling
|
||||
|
||||
This setting selects words randomly from the list of possible words, with each word having an equal chance of being selected. This method can produce text that is more diverse but may also be less coherent.
|
||||
|
||||
Set value to 1 to disable its effect.
|
||||
|
||||
#### Tail Free Sampling
|
||||
|
||||
This setting removes the least probable words from consideration during text generation, which can improve the quality and coherence of the generated text.
|
||||
|
||||
Set value to 1 to disable its effect.
|
||||
|
||||
#### Repetition Penalty Slope
|
||||
|
||||
If both this and Repetition Penalty Range are above 0, then repetition penalty will have more effect closer to the end of the prompt. The higher the value, the stronger the effect.
|
||||
|
||||
Set value to 1 for linear interpolation or 0 to disable interpolation.
|
||||
|
||||
### Soft Prompts
|
||||
|
||||
**Soft Prompts allow you to customize the style and behavior of your AI.**
|
||||
|
||||
They are created by training the AI with a special type of prompt using a collection of input data. Experimenting with different soft prompts can lead to exciting and unique results. The most successful soft prompts are those that align the AI's output with a literary genre, fictional universe, or the style of a particular author.
|
||||
|
||||
#### Common Misconceptions
|
||||
|
||||
* Soft prompts do not provide new information to the model, but can effectively influence the model's tone, word choice, and formatting.
|
||||
* Soft prompts are not a means of compressing a full prompt into a limited token space. Instead, they provide a way to guide the language model's output through data in the context.
|
||||
|
||||
## NovelAI
|
||||
|
||||
### API Key
|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
5. Select the second request, then in the Headers tab of the inspection panel, scroll down to the very bottom. Look for a header called Authorization:
|
||||
|
||||

|
||||
|
||||
The long string (after "Bearer", not including it) is your API key.
|
||||
|
||||
* Proxies and Cloudflare-type services may interfere with connection.
|
||||
|
||||
### Settings
|
||||
|
||||
The files with the settings are here (SillyTavern\public\NovelAI Settings).
|
||||
You can also manually add your own settings files.
|
||||
|
||||
#### Temperature
|
||||
|
||||
Value from 0.1 to 2.0
|
||||
|
||||
Lower value - the answers are more logical, but less creative.
|
||||
|
||||
Higher value - the answers are more creative, but less logical.
|
||||
|
||||
#### Repetition penalty
|
||||
|
||||
Repetition penalty is responsible for the penalty of repeated words.
|
||||
If the character is fixated on something or repeats the same phrase, then increasing this parameter will fix it.
|
||||
It is not recommended to increase this parameter too much for the chat format, as it may break this format.
|
||||
|
||||
**The standard value for chat is approximately 1.0 - 1.05**
|
||||
|
||||
#### Repetition penalty range
|
||||
|
||||
The range of influence of Repetition penalty in tokens.
|
||||
|
||||
### Models
|
||||
|
||||
If your subscription tier is Paper, Tablet or Scroll use only Euterpe model otherwise you can not get an answer from NovelAI API.
|
||||
|
||||
## OpenAI
|
||||
|
||||
### API key
|
||||
|
||||
**How to get:**
|
||||
|
||||
1. Go to [OpenAI](https://platform.openai.com/) and sign in.
|
||||
2. Use "[View API keys](https://platform.openai.com/account/api-keys)" option to create a new API key.
|
||||
|
||||
**Important!**
|
||||
|
||||
_Lost API keys can't be restored! Make sure to keep it safe!_
|
||||
|
||||
### Window.ai
|
||||
|
||||
You can use Window.ai browser extension to access AI models with SillyTavern.
|
||||
|
||||
1. Install a browser extension from: [windowai.io](https://windowai.io/)
|
||||
2. Select OpenAI in SillyTavern's Connection panel and check the "Use Window.ai" option.
|
||||
3. Use the extension to pick which API to connect to.
|
||||
|
||||
Don't have OpenAI / Claude API access? Use OpenRouter.
|
||||
|
||||
1. Create an OpenRouter account: [openrouter.ai](https://openrouter.ai/)
|
||||
2. Select OpenRouter as a provider in Window.ai extension.
|
||||
|
||||
OpenRouter works by letting you use keys that they own. It has a free trial, and paid access afterwards.
|
||||
|
||||
## Poe
|
||||
|
||||
### API key
|
||||
|
||||
**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. 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 "<http://poe.com/>" 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
|
||||
|
||||
This feature is considered obsolete and has been removed.
|
||||
|
||||
The use of the Author's Note extension is now a preferred way to add prompt injections of variable depth.
|
||||
|
||||
## Instruct Mode
|
||||
|
||||
Instruct Mode allows you to adjust the prompting for instruction-following models, such as Alpaca, Metharme, WizardLM, etc.
|
||||
|
||||
**This is not supported for OpenAI API.**
|
||||
|
||||
### Instruct Mode Settings
|
||||
|
||||
#### System Prompt
|
||||
|
||||
Added to the beginning of each prompt. Should define the instructions for the model to follow.
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
Write one reply in internet RP style for {{char}}. Be verbose and creative.
|
||||
```
|
||||
|
||||
#### Presets
|
||||
|
||||
Provides ready-made presets with prompts and sequences for some well-known instruct models.
|
||||
|
||||
_Changing a preset resets your system prompt to default!_
|
||||
|
||||
#### Input Sequence
|
||||
|
||||
Text added before the user's input.
|
||||
|
||||
#### Output Sequence
|
||||
|
||||
Text added before the character's reply.
|
||||
|
||||
#### System Sequence
|
||||
|
||||
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.
|
||||
|
||||
#### Include Names
|
||||
|
||||
If enabled, prepend character and user names to chat history logs after inserting the sequences.
|
||||
|
||||
_Always enabled for group chats!_
|
||||
|
||||
#### Wrap Sequences with Newline
|
||||
|
||||
Each sequence text will be wrapped with newline characters when inserted to the prompt. Required for Alpaca and its derivatives.
|
||||
|
||||
## Chat import
|
||||
|
||||
**Import chats into SillyTavern**
|
||||
|
||||
To import Character.AI chats, use this tool: [https://github.com/0x000011b/characterai-dumper](https://github.com/0x000011b/characterai-dumper).
|
||||
|
||||
## Tokenizer
|
||||
|
||||
**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.
|
||||
|
||||
SillyTavern can use the following tokenizers while forming a request to the AI backend:
|
||||
|
||||
1. None. Each token is estimated to be ~3.3 characters, rounded up to the nearest integer. **Try this if your prompts get cut off on high context lengths.** This approach is used by KoboldAI Lite.
|
||||
2. GPT-3 tokenizer. **Use to get more accurate counts on OpenAI character cards.** Can be previewed here: [OpenAI Tokenizer](https://platform.openai.com/tokenizer).
|
||||
3. (Legacy) GPT-2/3 tokenizer. Used by original TavernAI. **Pick this if you're unsure.** More info: [gpt-2-3-tokenizer](https://github.com/josephrocca/gpt-2-3-tokenizer).
|
||||
4. Sentencepiece tokenizer. Used by LLaMA model family: Alpaca, Vicuna, Koala, etc. **Pick if you use a LLaMA model.**
|
||||
|
||||
## Token Padding
|
||||
|
||||
**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.
|
||||
|
||||
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.
|
||||
|
||||
To prevent this, SillyTavern allocates a portion of the context size as padding to avoid adding more chat items than the model can accommodate. If you find that some part of the prompt is trimmed even with the most-matching tokenizer selected, adjust the padding so the description is not truncated.
|
||||
|
||||
You can input negative values for reverse padding, which allows allocating more than the set maximum amount of tokens.
|
||||
|
||||
## Advanced Formatting
|
||||
|
||||
The settings provided in this section allow for more control over the prompt building strategy. Most specifics of the prompt building depend on whether a Pygmalion model is selected or special formatting is force-enabled. The core differences between the formatting schemas are listed below.
|
||||
|
||||
### Custom Chat Separator
|
||||
|
||||
Overrides the default separators controlled by "Disable example chats formatting" and "Disable chat start formatting" options (see below).
|
||||
|
||||
### For _Pygmalion_ formatting
|
||||
|
||||
#### Disable description formatting
|
||||
|
||||
`**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.
|
||||
|
||||
#### Disable personality formatting
|
||||
|
||||
`**Personality:**`won't be prepended to the content of your character's Personality box.
|
||||
|
||||
#### Disable example chats formatting
|
||||
|
||||
`<START>` won't be added at the beginning of each example message block.
|
||||
_(If custom separator is not set)_
|
||||
|
||||
#### Disable chat start formatting
|
||||
|
||||
`<START>` 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
|
||||
|
||||
Doesn't do anything (Included in Pygmalion formatting).
|
||||
|
||||
### For _non-Pygmalion_ formatting
|
||||
|
||||
#### Disable description formatting
|
||||
|
||||
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.
|
||||
|
||||
#### Disable personality formatting
|
||||
|
||||
`**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.
|
||||
_(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.
|
||||
_(If custom separator is not set)_
|
||||
|
||||
#### Always add character's name to prompt
|
||||
|
||||
Appends character's name to the prompt to force the model to complete the message as the character:
|
||||
|
||||
```
|
||||
** OTHER CONTEXT HERE **
|
||||
Character:
|
||||
```
|
||||
|
||||
## Group Chats
|
||||
|
||||
### Reply order strategies
|
||||
|
||||
Decides how characters in group chats are drafted for their replies.
|
||||
|
||||
#### Natural order
|
||||
|
||||
Tries to simulate the flow of a real human conversation. The algorithm is as follows:
|
||||
|
||||
1. Mentions of the group member names are extracted from the last message in chat.
|
||||
|
||||
Only whole words are recognized as mentions! If your character's name is "Misaka Mikoto", they will reply only activate on "Misaka" or "Mikoto", but never to "Misa", "Railgun", etc.
|
||||
|
||||
Unless "Allow bot responses to self" setting is enabled, characters won't reply to mentions of their name in their own message!
|
||||
|
||||
2. Characters are activated by the "Talkativeness" factor.
|
||||
|
||||
Talkativeness defines how often the character speaks if they were not mentioned. Adjust this value on "Advanced definitions" screen in character editor. Slider values are on a linear scale from **0% / Shy** (character never talks unless mentioned) to **100% / Chatty** (character always replies). Default value for new characters is 50% chance.
|
||||
|
||||
3. Random character is selected.
|
||||
|
||||
If no characters were activated at previous steps, one speaker is selected randomly, ignoring all other conditions.
|
||||
|
||||
#### List order
|
||||
|
||||
Characters are drafted based on the order they are presented in group members list. No other rules apply.
|
||||
|
||||
## 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._
|
||||
|
||||
SillyTavern tries to create faster and longer responses by chaining the generation using smaller batches.
|
||||
|
||||
### Default settings
|
||||
|
||||
First batch = 50 tokens
|
||||
|
||||
Next batches = 30 tokens
|
||||
|
||||
### 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
|
||||
|
||||
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)
|
||||
|
||||
## User Settings
|
||||
|
||||
### Message Sound
|
||||
|
||||
To play your own custom sound on receiving a new message from bot, replace the following MP3 file in your SillyTavern folder:
|
||||
|
||||
`public/sounds/message.mp3`
|
||||
|
||||
Plays at 80% volume.
|
||||
|
||||
If "Background Sound Only" option is enabled, the sound plays only if SillyTavern window is **unfocused**.
|
||||
|
||||
### Formulas Rendering
|
||||
|
||||
Enables math formulas rendering using the [showdown-katex](https://obedm503.github.io/showdown-katex/) package.
|
||||
|
||||
The following formatting rules are supported:
|
||||
|
||||
#### LaTeX syntax
|
||||
|
||||
```
|
||||
$$ formula goes here $$
|
||||
```
|
||||
|
||||
#### Asciimath syntax
|
||||
|
||||
```
|
||||
formula goes here $
|
||||
```
|
||||
|
||||
More information: [KaTeX](https://katex.org/)
|
@ -1,24 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>SillyTavern FAQ</title>
|
||||
<link rel="stylesheet" href="/css/notes.css">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="/webfonts/NotoSans/stylesheet.css" rel="stylesheet">
|
||||
<script src="/scripts/showdown.min.js"></script>
|
||||
<script src="/scripts/showdown-toc.min.js"></script>
|
||||
<script src="/scripts/notes.js"></script>
|
||||
</head>
|
||||
|
||||
<body onload="loadNotes('/get_faq')">
|
||||
<div id="main">
|
||||
<div id="content">
|
||||
<!-- To change the guidebook content edit the content.md file -->
|
||||
<!-- Then it will be dynamically inserted here -->
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -2,21 +2,20 @@
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>SillyTavern Guidebook</title>
|
||||
<title>SillyTavern Documentation</title>
|
||||
<link rel="stylesheet" href="/css/notes.css">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="/webfonts/NotoSans/stylesheet.css" rel="stylesheet">
|
||||
<script src="/scripts/showdown.min.js"></script>
|
||||
<script src="/scripts/showdown-toc.min.js"></script>
|
||||
<script src="/scripts/notes.js"></script>
|
||||
</head>
|
||||
|
||||
<body onload="loadNotes('/notes/content.md')">
|
||||
<body>
|
||||
<div id="main">
|
||||
<div id="content">
|
||||
<!-- To change the guidebook content edit the content.md file -->
|
||||
<!-- Then it will be dynamically inserted here -->
|
||||
<h2>You weren't supposed to be able to get here, you know.</h1>
|
||||
<h3>All help materials has been moved here:</h3>
|
||||
<h3><a href="https://docs.sillytavern.app/">SillyTavern Documentation</a></h3>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
@ -1,24 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>SillyTavern Guidebook</title>
|
||||
<link rel="stylesheet" href="/css/notes.css">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="/webfonts/NotoSans/stylesheet.css" rel="stylesheet">
|
||||
<script src="/scripts/showdown.min.js"></script>
|
||||
<script src="/scripts/showdown-toc.min.js"></script>
|
||||
<script src="/scripts/notes.js"></script>
|
||||
</head>
|
||||
|
||||
<body onload="loadNotes('/get_readme')">
|
||||
<div id="main">
|
||||
<div id="content">
|
||||
<!-- To change the guidebook content edit the content.md file -->
|
||||
<!-- Then it will be dynamically inserted here -->
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,24 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>SillyTavern Guidebook</title>
|
||||
<link rel="stylesheet" href="/css/notes.css">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="/webfonts/NotoSans/stylesheet.css" rel="stylesheet">
|
||||
<script src="/scripts/showdown.min.js"></script>
|
||||
<script src="/scripts/showdown-toc.min.js"></script>
|
||||
<script src="/scripts/notes.js"></script>
|
||||
</head>
|
||||
|
||||
<body onload="loadNotes('/notes/update.md')">
|
||||
<div id="main">
|
||||
<div id="content">
|
||||
<!-- To change the guidebook content edit the content.md file -->
|
||||
<!-- Then it will be dynamically inserted here -->
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,65 +0,0 @@
|
||||
# How to Update SillyTavern
|
||||
|
||||
This is not an installation guide. If you need installation instructions, look here:
|
||||
<https://docs.alpindale.dev/pygmalion-extras/sillytavern/#installation>
|
||||
|
||||
(This guide assumes you have already installed SillyTavern once and know how to run it on your OS.)
|
||||
|
||||
(A plain text copy of this file is also present inside SillyTavern's base install folder.)
|
||||
|
||||
----
|
||||
|
||||
## Linux/Termux
|
||||
|
||||
You definitely installed via git, so just 'git pull' inside the SillyTavern directory.
|
||||
|
||||
----
|
||||
|
||||
## Windows/MacOS
|
||||
|
||||
### 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.
|
||||
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.
|
||||
|
||||
### Method 2 - ZIP
|
||||
|
||||
If you insist on installing via a zip, here is the tedious process for doing the update:
|
||||
|
||||
1. Download the new release zip.
|
||||
2. Unzip it into a folder OUTSIDE of your current ST installation.
|
||||
3. Do the usual setup procedure for your OS to install NodeJS requirements.
|
||||
|
||||
4. Copy the following files/folders as necessary(*) from your old ST installation:
|
||||
|
||||
(*) 'As necessary' = "If you made any custom content related to those folders".
|
||||
None of the folders are mandatory, so only copy what you need.
|
||||
|
||||
#### NB: DO NOT COPY THE ENTIRE /PUBLIC/ FOLDER
|
||||
|
||||
Doing so could break the new install and prevent new features from being present.
|
||||
|
||||
```plaintext
|
||||
Backgrounds
|
||||
Characters
|
||||
Chats
|
||||
Groups
|
||||
Group chats
|
||||
KoboldAI Settings
|
||||
NovelAI Settings
|
||||
OpenAI Settings
|
||||
TextGen Settings (textgen = ooba)
|
||||
Themes
|
||||
User Avatars
|
||||
Worlds
|
||||
settings.json
|
||||
```
|
||||
|
||||
5. Once those folders/files are copied, Paste them into the /Public/ folder of the new install.
|
||||
|
||||
6. Start SillyTavern once again with the method appropriate to your OS, and pray you got it right.
|
||||
|
||||
7. If everything shows up, you can safely delete the old ST folder.
|
1715
public/script.js
1715
public/script.js
File diff suppressed because it is too large
Load Diff
@ -21,13 +21,14 @@ import {
|
||||
send_on_enter_options,
|
||||
} from "./power-user.js";
|
||||
|
||||
import { LoadLocal, SaveLocal, ClearLocal, CheckLocal, LoadLocalBool } from "./f-localStorage.js";
|
||||
import { LoadLocal, SaveLocal, CheckLocal, LoadLocalBool } from "./f-localStorage.js";
|
||||
import { selected_group, is_group_generating, getGroupAvatar, groups } from "./group-chats.js";
|
||||
import {
|
||||
SECRET_KEYS,
|
||||
secret_state,
|
||||
} from "./secrets.js";
|
||||
import { sortByCssOrder } from "./utils.js";
|
||||
import { sortByCssOrder, debounce } from "./utils.js";
|
||||
import { chat_completion_sources, oai_settings } from "./openai.js";
|
||||
|
||||
var NavToggle = document.getElementById("nav-toggle");
|
||||
|
||||
@ -60,6 +61,7 @@ var retry_delay = 500;
|
||||
var RA_AC_retries = 1;
|
||||
|
||||
const observerConfig = { childList: true, subtree: true };
|
||||
const countTokensDebounced = debounce(RA_CountCharTokens, 1000);
|
||||
|
||||
const observer = new MutationObserver(function (mutations) {
|
||||
mutations.forEach(function (mutation) {
|
||||
@ -70,7 +72,6 @@ const observer = new MutationObserver(function (mutations) {
|
||||
} else if (mutation.target.parentNode === SelectedCharacterTab) {
|
||||
setTimeout(RA_CountCharTokens, 200);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@ -191,9 +192,9 @@ $("#rm_button_create").on("click", function () { //when "+New Ch
|
||||
$("#result_info").html('Type to start counting tokens!');
|
||||
});
|
||||
//when any input is made to the create/edit character form textareas
|
||||
$("#rm_ch_create_block").on("input", function () { RA_CountCharTokens(); });
|
||||
$("#rm_ch_create_block").on("input", function () { countTokensDebounced(); });
|
||||
//when any input is made to the advanced editing popup textareas
|
||||
$("#character_popup").on("input", function () { RA_CountCharTokens(); });
|
||||
$("#character_popup").on("input", function () { countTokensDebounced(); });
|
||||
//function:
|
||||
export function RA_CountCharTokens() {
|
||||
$("#result_info").html("");
|
||||
@ -259,16 +260,23 @@ export function RA_CountCharTokens() {
|
||||
(power_user.pin_examples ? characters[this_chid].mes_example : ''),
|
||||
].join('\n').replace(/\r/gm, '').trim();
|
||||
perm_tokens = getTokenCount(perm_string);
|
||||
} else { console.log("RA_TC -- no valid char found, closing."); } // if neither, probably safety char or some error in loading
|
||||
// if neither, probably safety char or some error in loading
|
||||
} else { console.debug("RA_TC -- no valid char found, closing."); }
|
||||
}
|
||||
// display the counted tokens
|
||||
if (count_tokens < 1024 && perm_tokens < 1024) {
|
||||
$("#result_info").html(count_tokens + " Tokens (" + perm_tokens + " Permanent Tokens)"); //display normal if both counts are under 1024
|
||||
//display normal if both counts are under 1024
|
||||
$("#result_info").html(`<small>${count_tokens} Tokens (${perm_tokens} Permanent)</small>`);
|
||||
} else {
|
||||
$("#result_info").html(`
|
||||
<span class="neutral_warning">${count_tokens}</span> Tokens (<span class="neutral_warning">${perm_tokens}</span><span> Permanent Tokens)
|
||||
<br>
|
||||
<div id="chartokenwarning" class="menu_button whitespacenowrap"><a href="/notes#charactertokens" target="_blank">Learn More About Token 'Limits'</a></div>`);
|
||||
<div class="flex-container flexFlowColumn alignitemscenter">
|
||||
<div class="flex-container flexnowrap flexNoGap">
|
||||
<small class="flex-container flexnowrap flexNoGap">
|
||||
<div class="neutral_warning">${count_tokens}</div> Tokens (<div class="neutral_warning">${perm_tokens}</div><div> Permanent)</div>
|
||||
</small>
|
||||
</div>
|
||||
<div id="chartokenwarning" class="menu_button whitespacenowrap"><a href="https://docs.sillytavern.app/usage/core-concepts/characterdesign/#character-tokens" target="_blank">About Token 'Limits'</a></div>
|
||||
</div>`);
|
||||
} //warn if either are over 1024
|
||||
}
|
||||
//Auto Load Last Charcter -- (fires when active_character is defined and auto_load_chat is true)
|
||||
@ -348,7 +356,7 @@ function RA_checkOnlineStatus() {
|
||||
connection_made = false;
|
||||
} else {
|
||||
if (online_status !== undefined && online_status !== "no_connection") {
|
||||
$("#send_textarea").attr("placeholder", "Type a message..."); //on connect, placeholder tells user to type message
|
||||
$("#send_textarea").attr("placeholder", `Type a message, or /? for command list`); //on connect, placeholder tells user to type message
|
||||
$('#send_form').removeClass("no-connection");
|
||||
$("#API-status-top").removeClass("fa-plug-circle-exclamation redOverlayGlow");
|
||||
$("#API-status-top").addClass("fa-plug");
|
||||
@ -388,7 +396,7 @@ function RA_autoconnect(PrevApi) {
|
||||
}
|
||||
break;
|
||||
case 'openai':
|
||||
if (secret_state[SECRET_KEYS.OPENAI]) {
|
||||
if (secret_state[SECRET_KEYS.OPENAI] || secret_state[SECRET_KEYS.CLAUDE] || oai_settings.chat_completion_source == chat_completion_sources.WINDOWAI) {
|
||||
$("#api_button_openai").click();
|
||||
}
|
||||
break;
|
||||
@ -428,13 +436,13 @@ function OpenNavPanels() {
|
||||
|
||||
//auto-open L nav if locked and previously open
|
||||
if (LoadLocalBool("LNavLockOn") == true && LoadLocalBool("LNavOpened") == true) {
|
||||
console.log("RA -- clicking left nav to open");
|
||||
console.debug("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");
|
||||
console.debug("RA -- clicking WI to open");
|
||||
$("#WIDrawerIcon").click();
|
||||
}
|
||||
}
|
||||
@ -450,7 +458,7 @@ dragElement(document.getElementById("WorldInfo"));
|
||||
|
||||
|
||||
|
||||
function dragElement(elmnt) {
|
||||
export function dragElement(elmnt) {
|
||||
|
||||
var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
|
||||
if (document.getElementById(elmnt.id + "header")) { //ex: id="sheldheader"
|
||||
@ -504,7 +512,7 @@ function dragElement(elmnt) {
|
||||
pos3 = e.clientX; //new mouse X
|
||||
pos4 = e.clientY; //new mouse Y
|
||||
|
||||
|
||||
elmnt.setAttribute('data-dragged', 'true');
|
||||
|
||||
//fix over/underflows:
|
||||
|
||||
@ -651,14 +659,14 @@ $("document").ready(function () {
|
||||
$(WIPanelPin).on("click", function () {
|
||||
SaveLocal("WINavLockOn", $(WIPanelPin).prop("checked"));
|
||||
if ($(WIPanelPin).prop("checked") == true) {
|
||||
console.log('adding pin class to WI');
|
||||
console.debug('adding pin class to WI');
|
||||
$(WorldInfo).addClass('pinnedOpen');
|
||||
} else {
|
||||
console.log('removing pin class from WI');
|
||||
console.debug('removing pin class from WI');
|
||||
$(WorldInfo).removeClass('pinnedOpen');
|
||||
|
||||
if ($(WorldInfo).hasClass('openDrawer') && $('.openDrawer').length > 1) {
|
||||
console.log('closing WI after lock removal');
|
||||
console.debug('closing WI after lock removal');
|
||||
$(WorldInfo).slideToggle(200, "swing");
|
||||
//$(WorldInfoDrawerIcon).toggleClass('openIcon closedIcon');
|
||||
$(WorldInfo).toggleClass('openDrawer closedDrawer');
|
||||
@ -673,7 +681,7 @@ $("document").ready(function () {
|
||||
$(RightNavPanel).addClass('pinnedOpen');
|
||||
}
|
||||
if ($(RPanelPin).prop('checked' == true)) {
|
||||
console.log('setting pin class via checkbox state');
|
||||
console.debug('setting pin class via checkbox state');
|
||||
$(RightNavPanel).addClass('pinnedOpen');
|
||||
}
|
||||
// read the state of left Nav Lock and apply to leftnav classlist
|
||||
@ -683,7 +691,7 @@ $("document").ready(function () {
|
||||
$(LeftNavPanel).addClass('pinnedOpen');
|
||||
}
|
||||
if ($(LPanelPin).prop('checked' == true)) {
|
||||
console.log('setting pin class via checkbox state');
|
||||
console.debug('setting pin class via checkbox state');
|
||||
$(LeftNavPanel).addClass('pinnedOpen');
|
||||
}
|
||||
|
||||
@ -695,7 +703,7 @@ $("document").ready(function () {
|
||||
}
|
||||
|
||||
if ($(WIPanelPin).prop('checked' == true)) {
|
||||
console.log('setting pin class via checkbox state');
|
||||
console.debug('setting pin class via checkbox state');
|
||||
$(WorldInfo).addClass('pinnedOpen');
|
||||
}
|
||||
|
||||
@ -805,16 +813,40 @@ $("document").ready(function () {
|
||||
Generate();
|
||||
}
|
||||
}
|
||||
|
||||
if (event.ctrlKey && event.key == "Enter") {
|
||||
// Ctrl+Enter for Regeneration Last Response
|
||||
if (is_send_press == false) {
|
||||
$('#option_regenerate').click();
|
||||
$('#options').hide();
|
||||
}
|
||||
//ctrl+shift+up to scroll to context line
|
||||
if (event.shiftKey && event.ctrlKey && event.key == "ArrowUp") {
|
||||
event.preventDefault();
|
||||
let contextLine = $('.lastInContext');
|
||||
if (contextLine.length !== 0) {
|
||||
$('#chat').animate({
|
||||
scrollTop: contextLine.offset().top - $('#chat').offset().top + $('#chat').scrollTop()
|
||||
}, 300);
|
||||
} else { toastr.warning('Context line not found, send a message first!'); }
|
||||
}
|
||||
//ctrl+shift+down to scroll to bottom of chat
|
||||
if (event.shiftKey && event.ctrlKey && event.key == "ArrowDown") {
|
||||
event.preventDefault();
|
||||
$('#chat').animate({
|
||||
scrollTop: $('#chat').prop('scrollHeight')
|
||||
}, 300);
|
||||
}
|
||||
|
||||
if (event.ctrlKey && event.key == "ArrowLeft") { //for debug, show all local stored vars
|
||||
// Ctrl+Enter for Regeneration Last Response. If editing, accept the edits instead
|
||||
if (event.ctrlKey && event.key == "Enter") {
|
||||
const editMesDone = $(".mes_edit_done:visible");
|
||||
if (editMesDone.length > 0) {
|
||||
console.debug("Accepting edits with Ctrl+Enter");
|
||||
editMesDone.trigger('click');
|
||||
} else if (is_send_press == false) {
|
||||
console.debug("Regenerating with Ctrl+Enter");
|
||||
$('#option_regenerate').click();
|
||||
$('#options').hide();
|
||||
} else {
|
||||
console.debug("Ctrl+Enter ignored");
|
||||
}
|
||||
}
|
||||
//ctrl+left to show all local stored vars (debug)
|
||||
if (event.ctrlKey && event.key == "ArrowLeft") {
|
||||
CheckLocal();
|
||||
}
|
||||
|
||||
@ -842,7 +874,7 @@ $("document").ready(function () {
|
||||
}
|
||||
|
||||
if (event.ctrlKey && event.key == "ArrowUp") { //edits last USER message if chatbar is empty and focused
|
||||
console.log('got ctrl+uparrow input');
|
||||
console.debug('got ctrl+uparrow input');
|
||||
if (
|
||||
$("#send_textarea").val() === '' &&
|
||||
chatbarInFocus === true &&
|
||||
|
@ -201,7 +201,7 @@ async function convertSoloToGroupChat() {
|
||||
const members = [character.avatar];
|
||||
const activationStrategy = group_activation_strategy.NATURAL;
|
||||
const allowSelfResponses = false;
|
||||
const favChecked = character.fav == 'true';
|
||||
const favChecked = character.fav || character.fav == 'true';
|
||||
const metadata = Object.assign({}, chat_metadata);
|
||||
delete metadata.main_chat;
|
||||
|
||||
|
@ -49,6 +49,8 @@ EventEmitter.prototype.removeListener = function (event, listener) {
|
||||
};
|
||||
|
||||
EventEmitter.prototype.emit = async function (event) {
|
||||
console.debug('Event emitted: ' + event);
|
||||
|
||||
var i, listeners, length, args = [].slice.call(arguments, 1);
|
||||
|
||||
if (typeof this.events[event] === 'object') {
|
||||
|
@ -1,26 +1,56 @@
|
||||
import { callPopup, eventSource, event_types, saveSettings, saveSettingsDebounced } from "../script.js";
|
||||
import { isSubsetOf } from "./utils.js";
|
||||
import { isSubsetOf, debounce } from "./utils.js";
|
||||
export {
|
||||
getContext,
|
||||
getApiUrl,
|
||||
loadExtensionSettings,
|
||||
runGenerationInterceptors,
|
||||
defaultRequestArgs,
|
||||
doExtrasFetch,
|
||||
modules,
|
||||
extension_settings,
|
||||
ModuleWorkerWrapper,
|
||||
};
|
||||
|
||||
let extensionNames = [];
|
||||
let manifests = [];
|
||||
const defaultUrl = "http://localhost:5100";
|
||||
export const saveMetadataDebounced = debounce(async () => await getContext().saveMetadata(), 1000);
|
||||
|
||||
// Disables parallel updates
|
||||
class ModuleWorkerWrapper {
|
||||
constructor(callback) {
|
||||
this.isBusy = false;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
// Called by the extension
|
||||
async update() {
|
||||
// Don't touch me I'm busy...
|
||||
if (this.isBusy) {
|
||||
return;
|
||||
}
|
||||
|
||||
// I'm free. Let's update!
|
||||
try {
|
||||
this.isBusy = true;
|
||||
await this.callback();
|
||||
}
|
||||
finally {
|
||||
this.isBusy = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const extension_settings = {
|
||||
apiUrl: defaultUrl,
|
||||
apiKey: '',
|
||||
autoConnect: false,
|
||||
disabledExtensions: [],
|
||||
expressionOverrides: [],
|
||||
memory: {},
|
||||
note: {
|
||||
default: '',
|
||||
chara: [],
|
||||
},
|
||||
caption: {},
|
||||
expressions: {},
|
||||
@ -29,6 +59,8 @@ const extension_settings = {
|
||||
sd: {},
|
||||
chromadb: {},
|
||||
translate: {},
|
||||
objective: {},
|
||||
quickReply: {},
|
||||
};
|
||||
|
||||
let modules = [];
|
||||
@ -36,9 +68,45 @@ let activeExtensions = new Set();
|
||||
|
||||
const getContext = () => window['SillyTavern'].getContext();
|
||||
const getApiUrl = () => extension_settings.apiUrl;
|
||||
const defaultRequestArgs = { method: 'GET', headers: { 'Bypass-Tunnel-Reminder': 'bypass' } };
|
||||
let connectedToApi = false;
|
||||
|
||||
function showHideExtensionsMenu() {
|
||||
// Get the number of menu items that are not hidden
|
||||
const hasMenuItems = $('#extensionsMenu').children().filter((_, child) => $(child).css('display') !== 'none').length > 0;
|
||||
|
||||
// We have menu items, so we can stop checking
|
||||
if (hasMenuItems) {
|
||||
clearInterval(menuInterval);
|
||||
}
|
||||
|
||||
// Show or hide the menu button
|
||||
$('#extensionsMenuButton').toggle(hasMenuItems);
|
||||
}
|
||||
|
||||
// Periodically check for new extensions
|
||||
const menuInterval = setInterval(showHideExtensionsMenu, 1000);
|
||||
|
||||
async function doExtrasFetch(endpoint, args) {
|
||||
if (!args) {
|
||||
args = {}
|
||||
}
|
||||
|
||||
if (!args.method) {
|
||||
Object.assign(args, { method: 'GET' });
|
||||
}
|
||||
|
||||
if (!args.headers) {
|
||||
args.headers = {}
|
||||
}
|
||||
Object.assign(args.headers, {
|
||||
'Authorization': `Bearer ${extension_settings.apiKey}`,
|
||||
'Bypass-Tunnel-Reminder': 'bypass'
|
||||
});
|
||||
|
||||
const response = await fetch(endpoint, args);
|
||||
return response;
|
||||
}
|
||||
|
||||
async function discoverExtensions() {
|
||||
try {
|
||||
const response = await fetch('/discover_extensions');
|
||||
@ -81,19 +149,29 @@ async function disableExtension(name) {
|
||||
|
||||
async function getManifests(names) {
|
||||
const obj = {};
|
||||
for (const name of names) {
|
||||
const response = await fetch(`/scripts/extensions/${name}/manifest.json`);
|
||||
const promises = [];
|
||||
|
||||
if (response.ok) {
|
||||
const json = await response.json();
|
||||
obj[name] = json;
|
||||
}
|
||||
for (const name of names) {
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
fetch(`/scripts/extensions/${name}/manifest.json`).then(async response => {
|
||||
if (response.ok) {
|
||||
const json = await response.json();
|
||||
obj[name] = json;
|
||||
resolve();
|
||||
}
|
||||
}).catch(err => reject() && console.log('Could not load manifest.json for ' + name, err));
|
||||
});
|
||||
|
||||
promises.push(promise);
|
||||
}
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
return obj;
|
||||
}
|
||||
|
||||
async function activateExtensions() {
|
||||
const extensions = Object.entries(manifests).sort((a, b) => a[1].loading_order - b[1].loading_order);
|
||||
const promises = [];
|
||||
|
||||
for (let entry of extensions) {
|
||||
const name = entry[0];
|
||||
@ -111,9 +189,11 @@ async function activateExtensions() {
|
||||
const li = document.createElement('li');
|
||||
|
||||
if (!isDisabled) {
|
||||
await addExtensionScript(name, manifest);
|
||||
await addExtensionStyle(name, manifest);
|
||||
activeExtensions.add(name);
|
||||
const promise = Promise.all([addExtensionScript(name, manifest), addExtensionStyle(name, manifest)]);
|
||||
promise
|
||||
.then(() => activeExtensions.add(name))
|
||||
.catch(err => console.log('Could not activate extension: ' + name, err));
|
||||
promises.push(promise);
|
||||
}
|
||||
else {
|
||||
li.classList.add('disabled');
|
||||
@ -130,11 +210,15 @@ async function activateExtensions() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
}
|
||||
|
||||
async function connectClickHandler() {
|
||||
const baseUrl = $("#extensions_url").val();
|
||||
extension_settings.apiUrl = baseUrl;
|
||||
const testApiKey = $("#extensions_api_key").val();
|
||||
extension_settings.apiKey = testApiKey;
|
||||
saveSettingsDebounced();
|
||||
await connectToApi(baseUrl);
|
||||
}
|
||||
@ -152,8 +236,8 @@ function autoConnectInputHandler() {
|
||||
|
||||
function addExtensionsButtonAndMenu() {
|
||||
const buttonHTML =
|
||||
`<div id="extensionsMenuButton" class="fa-solid fa-magic-wand-sparkles" title="Extras Extensions" /></div>`;
|
||||
const extensionsMenuHTML = `<div id="extensionsMenu" class="list-group"></div>`;
|
||||
`<div id="extensionsMenuButton" style="display: none;" class="fa-solid fa-magic-wand-sparkles" title="Extras Extensions" /></div>`;
|
||||
const extensionsMenuHTML = `<div id="extensionsMenu" class="options-content" style="display: none;"></div>`;
|
||||
|
||||
$(document.body).append(extensionsMenuHTML);
|
||||
|
||||
@ -161,26 +245,42 @@ function addExtensionsButtonAndMenu() {
|
||||
|
||||
const button = $('#extensionsMenuButton');
|
||||
const dropdown = $('#extensionsMenu');
|
||||
dropdown.hide();
|
||||
//dropdown.hide();
|
||||
|
||||
let popper = Popper.createPopper(button.get(0), dropdown.get(0), {
|
||||
placement: 'top-end',
|
||||
});
|
||||
|
||||
$(document).on('click touchend', function (e) {
|
||||
const target = $(e.target);
|
||||
if (target.is(dropdown)) return;
|
||||
if (target.is(button) && !dropdown.is(":visible")) {
|
||||
e.preventDefault();
|
||||
$(button).on('click', function () {
|
||||
popper.update()
|
||||
dropdown.fadeIn(250);
|
||||
});
|
||||
|
||||
dropdown.show(200);
|
||||
popper.update();
|
||||
} else {
|
||||
dropdown.hide(200);
|
||||
$("html").on('touchstart mousedown', function (e) {
|
||||
let clickTarget = $(e.target);
|
||||
if (dropdown.is(':visible')
|
||||
&& clickTarget.closest(button).length == 0
|
||||
&& clickTarget.closest(dropdown).length == 0) {
|
||||
$(dropdown).fadeOut(250);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* $(document).on('click', function (e) {
|
||||
const target = $(e.target);
|
||||
if (target.is(dropdown)) return;
|
||||
if (target.is(button) && dropdown.is(':hidden')) {
|
||||
dropdown.toggle(200);
|
||||
popper.update();
|
||||
}
|
||||
if (target !== dropdown &&
|
||||
target !== button &&
|
||||
dropdown.is(":visible")) {
|
||||
dropdown.hide(200);
|
||||
}
|
||||
});
|
||||
} */
|
||||
|
||||
async function connectToApi(baseUrl) {
|
||||
if (!baseUrl) {
|
||||
return;
|
||||
@ -190,7 +290,7 @@ async function connectToApi(baseUrl) {
|
||||
url.pathname = '/api/modules';
|
||||
|
||||
try {
|
||||
const getExtensionsResult = await fetch(url, defaultRequestArgs);
|
||||
const getExtensionsResult = await doExtrasFetch(url);
|
||||
|
||||
if (getExtensionsResult.ok) {
|
||||
const data = await getExtensionsResult.json();
|
||||
@ -309,6 +409,7 @@ async function loadExtensionSettings(settings) {
|
||||
}
|
||||
|
||||
$("#extensions_url").val(extension_settings.apiUrl);
|
||||
$("#extensions_api_key").val(extension_settings.apiKey);
|
||||
$("#extensions_autoconnect").prop('checked', extension_settings.autoConnect);
|
||||
|
||||
// Activate offline extensions
|
||||
@ -326,7 +427,7 @@ async function runGenerationInterceptors(chat) {
|
||||
if (typeof window[interceptorKey] === 'function') {
|
||||
try {
|
||||
await window[interceptorKey](chat);
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
console.error(`Failed running interceptor for ${manifest.display_name}`, e);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,7 @@
|
||||
import { getContext } from "../../extensions.js";
|
||||
import { generateQuietPrompt } from "../../../script.js";
|
||||
import { getContext, saveMetadataDebounced } from "../../extensions.js";
|
||||
import { registerSlashCommand } from "../../slash-commands.js";
|
||||
import { stringFormat } from "../../utils.js";
|
||||
export { MODULE_NAME };
|
||||
|
||||
const MODULE_NAME = 'backgrounds';
|
||||
@ -51,13 +54,13 @@ function hasCustomBackground() {
|
||||
function saveBackgroundMetadata(file) {
|
||||
const context = getContext();
|
||||
context.chatMetadata[METADATA_KEY] = file;
|
||||
context.saveMetadata();
|
||||
saveMetadataDebounced();
|
||||
}
|
||||
|
||||
function removeBackgroundMetadata() {
|
||||
const context = getContext();
|
||||
delete context.chatMetadata[METADATA_KEY];
|
||||
context.saveMetadata();
|
||||
saveMetadataDebounced();
|
||||
}
|
||||
|
||||
function setCustomBackground() {
|
||||
@ -87,6 +90,30 @@ function onSelectBackgroundClick() {
|
||||
}
|
||||
}
|
||||
|
||||
const autoBgPrompt = `Pause your roleplay and choose a location ONLY from the provided list that is the most suitable for the current scene. Do not output any other text:\n{0}`;
|
||||
|
||||
async function autoBackgroundCommand() {
|
||||
const options = Array.from(document.querySelectorAll('.BGSampleTitle')).map(x => ({ element: x, text: x.innerText.trim() })).filter(x => x.text.length > 0);
|
||||
if (options.length == 0) {
|
||||
toastr.warning('No backgrounds to choose from. Please upload some images to the "backgrounds" folder.');
|
||||
return;
|
||||
}
|
||||
|
||||
const list = options.map(option => `- ${option.text}`).join('\n');
|
||||
const prompt = stringFormat(autoBgPrompt, list);
|
||||
const reply = await generateQuietPrompt(prompt);
|
||||
const fuse = new Fuse(options, { keys: ['text'] });
|
||||
const bestMatch = fuse.search(reply, { limit: 1 });
|
||||
|
||||
if (bestMatch.length == 0) {
|
||||
toastr.warning('No match found. Please try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.debug('Automatically choosing background:', bestMatch);
|
||||
bestMatch[0].item.element.click();
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
function addSettings() {
|
||||
const html = `
|
||||
@ -111,6 +138,16 @@ $(document).ready(function () {
|
||||
Any background image selected while lock is engaged will be saved automatically.
|
||||
</small>
|
||||
</div>
|
||||
<div class="background_controls">
|
||||
<div id="auto_background" class="menu_button">
|
||||
<i class="fa-solid fa-wand-magic"></i>
|
||||
Auto
|
||||
</div>
|
||||
<small>
|
||||
Automatically select a background based on the chat context.<br>
|
||||
Respects the "Lock" setting state.
|
||||
</small>
|
||||
</div>
|
||||
<div>Preview</div>
|
||||
<div id="custom_bg_preview">
|
||||
</div>
|
||||
@ -122,8 +159,12 @@ $(document).ready(function () {
|
||||
$('#lock_background').on('click', onLockBackgroundClick);
|
||||
$('#unlock_background').on('click', onUnlockBackgroundClick);
|
||||
$(document).on("click", ".bg_example", onSelectBackgroundClick);
|
||||
$('#auto_background').on("click", autoBackgroundCommand);
|
||||
}
|
||||
|
||||
addSettings();
|
||||
setInterval(moduleWorker, UPDATE_INTERVAL);
|
||||
registerSlashCommand('lockbg', onLockBackgroundClick, ['bglock'], " – locks a background for the currently selected chat", true, true);
|
||||
registerSlashCommand('unlockbg', onUnlockBackgroundClick, ['bgunlock'], ' – unlocks a background for the currently selected chat', true, true);
|
||||
registerSlashCommand('autobg', autoBackgroundCommand, ['bgauto'], ' – automatically changes the background based on the chat context using the AI request prompt', true, true);
|
||||
});
|
@ -7,5 +7,5 @@
|
||||
"css": "style.css",
|
||||
"author": "Cohee#1207",
|
||||
"version": "1.0.0",
|
||||
"homePage": "https://github.com/Cohee1207/SillyTavern"
|
||||
"homePage": "https://github.com/SillyTavern/SillyTavern"
|
||||
}
|
@ -1,16 +1,12 @@
|
||||
import { getBase64Async } from "../../utils.js";
|
||||
import { getContext, getApiUrl } from "../../extensions.js";
|
||||
import { getContext, getApiUrl, doExtrasFetch } from "../../extensions.js";
|
||||
export { MODULE_NAME };
|
||||
|
||||
const MODULE_NAME = 'caption';
|
||||
const UPDATE_INTERVAL = 1000;
|
||||
|
||||
async function moduleWorker() {
|
||||
const context = getContext();
|
||||
|
||||
context.onlineStatus === 'no_connection'
|
||||
? $('#send_picture').hide(200)
|
||||
: $('#send_picture').show(200);
|
||||
$('#send_picture').toggle(getContext().onlineStatus !== 'no_connection');
|
||||
}
|
||||
|
||||
async function setImageIcon() {
|
||||
@ -67,7 +63,7 @@ async function onSelectImage(e) {
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/caption';
|
||||
|
||||
const apiResult = await fetch(url, {
|
||||
const apiResult = await doExtrasFetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
@ -9,5 +9,5 @@
|
||||
"css": "style.css",
|
||||
"author": "Cohee#1207",
|
||||
"version": "1.0.0",
|
||||
"homePage": "https://github.com/Cohee1207/SillyTavern"
|
||||
"homePage": "https://github.com/SillyTavern/SillyTavern"
|
||||
}
|
@ -1,21 +1,20 @@
|
||||
import { callPopup } from "../../../script.js";
|
||||
import { getContext } from "../../extensions.js";
|
||||
import { registerSlashCommand } from "../../slash-commands.js";
|
||||
export { MODULE_NAME };
|
||||
|
||||
const MODULE_NAME = 'dice';
|
||||
const UPDATE_INTERVAL = 1000;
|
||||
|
||||
function setDiceIcon() {
|
||||
const sendButton = document.getElementById('roll_dice');
|
||||
/* sendButton.style.backgroundImage = `url(/img/dice-solid.svg)`; */
|
||||
//sendButton.classList.remove('spin');
|
||||
}
|
||||
|
||||
async function doDiceRoll() {
|
||||
let value = $(this).data('value');
|
||||
async function doDiceRoll(customDiceFormula) {
|
||||
let value = typeof customDiceFormula === 'string' ? customDiceFormula.trim() : $(this).data('value');
|
||||
|
||||
if (value == 'custom') {
|
||||
value = await callPopup('Enter the dice formula:<br><i>(for example, <tt>2d6</tt>)</i>', 'input');
|
||||
value = await callPopup('Enter the dice formula:<br><i>(for example, <tt>2d6</tt>)</i>', 'input');x
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isValid = droll.validate(value);
|
||||
@ -24,6 +23,8 @@ async function doDiceRoll() {
|
||||
const result = droll.roll(value);
|
||||
const context = getContext();
|
||||
context.sendSystemMessage('generic', `${context.name1} rolls a ${value}. The result is: ${result.total} (${result.rolls})`, { isSmallSys: true });
|
||||
} else {
|
||||
toastr.warning('Invalid dice formula');
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,7 +59,7 @@ function addDiceRollButton() {
|
||||
button.hide();
|
||||
|
||||
let popper = Popper.createPopper(button.get(0), dropdown.get(0), {
|
||||
placement: 'bottom',
|
||||
placement: 'top',
|
||||
});
|
||||
|
||||
$(document).on('click touchend', function (e) {
|
||||
@ -67,10 +68,10 @@ function addDiceRollButton() {
|
||||
if (target.is(button) && !dropdown.is(":visible")) {
|
||||
e.preventDefault();
|
||||
|
||||
dropdown.show(200);
|
||||
dropdown.fadeIn(250);
|
||||
popper.update();
|
||||
} else {
|
||||
dropdown.hide(200);
|
||||
dropdown.fadeOut(250);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -84,17 +85,13 @@ function addDiceScript() {
|
||||
}
|
||||
|
||||
async function moduleWorker() {
|
||||
const context = getContext();
|
||||
|
||||
context.onlineStatus === 'no_connection'
|
||||
? $('#roll_dice').hide(200)
|
||||
: $('#roll_dice').show(200);
|
||||
$('#roll_dice').toggle(getContext().onlineStatus !== 'no_connection');
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
jQuery(function () {
|
||||
addDiceScript();
|
||||
addDiceRollButton();
|
||||
setDiceIcon();
|
||||
moduleWorker();
|
||||
setInterval(moduleWorker, UPDATE_INTERVAL);
|
||||
registerSlashCommand('roll', (_, value) => doDiceRoll(value), ['r'], "<span class='monospace'>(dice formula)</span> – roll the dice. For example, /roll 2d6", false, true);
|
||||
});
|
@ -7,5 +7,5 @@
|
||||
"css": "style.css",
|
||||
"author": "Cohee#1207",
|
||||
"version": "1.0.0",
|
||||
"homePage": "https://github.com/Cohee1207/SillyTavern"
|
||||
"homePage": "https://github.com/SillyTavern/SillyTavern"
|
||||
}
|
@ -1,9 +1,13 @@
|
||||
import { callPopup, getRequestHeaders, saveSettingsDebounced } from "../../../script.js";
|
||||
import { getContext, getApiUrl, modules, extension_settings } from "../../extensions.js";
|
||||
import { callPopup, eventSource, event_types, getRequestHeaders, saveSettingsDebounced } from "../../../script.js";
|
||||
import { dragElement, isMobile } from "../../RossAscends-mods.js";
|
||||
import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch } from "../../extensions.js";
|
||||
import { power_user } from "../../power-user.js";
|
||||
import { onlyUnique, debounce, getCharaFilename } from "../../utils.js";
|
||||
export { MODULE_NAME };
|
||||
|
||||
const MODULE_NAME = 'expressions';
|
||||
const UPDATE_INTERVAL = 2000;
|
||||
const FALLBACK_EXPRESSION = 'joy';
|
||||
const DEFAULT_EXPRESSIONS = [
|
||||
"admiration",
|
||||
"amusement",
|
||||
@ -41,6 +45,256 @@ let lastMessage = null;
|
||||
let spriteCache = {};
|
||||
let inApiCall = false;
|
||||
|
||||
function isVisualNovelMode() {
|
||||
return Boolean(!isMobile() && power_user.waifuMode && getContext().groupId);
|
||||
}
|
||||
|
||||
async function forceUpdateVisualNovelMode() {
|
||||
if (isVisualNovelMode()) {
|
||||
await updateVisualNovelMode();
|
||||
}
|
||||
}
|
||||
|
||||
const updateVisualNovelModeDebounced = debounce(forceUpdateVisualNovelMode, 100);
|
||||
|
||||
async function updateVisualNovelMode(name, expression) {
|
||||
const container = $('#visual-novel-wrapper');
|
||||
|
||||
await visualNovelRemoveInactive(container);
|
||||
|
||||
const setSpritePromises = await visualNovelSetCharacterSprites(container, name, expression);
|
||||
|
||||
// calculate layer indices based on recent messages
|
||||
await visualNovelUpdateLayers(container);
|
||||
|
||||
await Promise.allSettled(setSpritePromises);
|
||||
|
||||
// update again based on new sprites
|
||||
if (setSpritePromises.length > 0) {
|
||||
await visualNovelUpdateLayers(container);
|
||||
}
|
||||
}
|
||||
|
||||
async function visualNovelRemoveInactive(container) {
|
||||
const context = getContext();
|
||||
const group = context.groups.find(x => x.id == context.groupId);
|
||||
const removeInactiveCharactersPromises = [];
|
||||
|
||||
// remove inactive characters after 1 second
|
||||
container.find('.expression-holder').each((_, current) => {
|
||||
const promise = new Promise(resolve => {
|
||||
const element = $(current);
|
||||
const avatar = element.data('avatar');
|
||||
|
||||
if (!group.members.includes(avatar) || group.disabled_members.includes(avatar)) {
|
||||
element.fadeOut(250, () => {
|
||||
element.remove();
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
removeInactiveCharactersPromises.push(promise);
|
||||
});
|
||||
|
||||
await Promise.allSettled(removeInactiveCharactersPromises);
|
||||
}
|
||||
|
||||
async function visualNovelSetCharacterSprites(container, name, expression) {
|
||||
const context = getContext();
|
||||
const group = context.groups.find(x => x.id == context.groupId);
|
||||
const labels = await getExpressionsList();
|
||||
|
||||
const createCharacterPromises = [];
|
||||
const setSpritePromises = [];
|
||||
|
||||
for (const avatar of group.members) {
|
||||
const isDisabled = group.disabled_members.includes(avatar);
|
||||
|
||||
// skip disabled characters
|
||||
if (isDisabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const character = context.characters.find(x => x.avatar == avatar);
|
||||
|
||||
if (!character) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let spriteFolderName = character.name;
|
||||
const avatarFileName = getSpriteFolderName({ original_avatar: character.avatar });
|
||||
const expressionOverride = extension_settings.expressionOverrides.find((e) =>
|
||||
e.name == avatarFileName
|
||||
);
|
||||
|
||||
if (expressionOverride && expressionOverride.path) {
|
||||
spriteFolderName = expressionOverride.path;
|
||||
}
|
||||
|
||||
// download images if not downloaded yet
|
||||
if (spriteCache[spriteFolderName] === undefined) {
|
||||
spriteCache[spriteFolderName] = await getSpritesList(spriteFolderName);
|
||||
}
|
||||
|
||||
const sprites = spriteCache[spriteFolderName];
|
||||
const expressionImage = container.find(`.expression-holder[data-avatar="${avatar}"]`);
|
||||
const defaultSpritePath = sprites.find(x => x.label === FALLBACK_EXPRESSION)?.path;
|
||||
const noSprites = sprites.length === 0;
|
||||
|
||||
if (expressionImage.length > 0) {
|
||||
if (name == spriteFolderName) {
|
||||
await validateImages(spriteFolderName, true);
|
||||
setExpressionOverrideHtml(true); // <= force clear expression override input
|
||||
const currentSpritePath = labels.includes(expression) ? sprites.find(x => x.label === expression)?.path : '';
|
||||
|
||||
const path = currentSpritePath || defaultSpritePath || '';
|
||||
const img = expressionImage.find('img');
|
||||
setImage(img, path);
|
||||
}
|
||||
expressionImage.toggleClass('hidden', noSprites);
|
||||
} else {
|
||||
const template = $('#expression-holder').clone();
|
||||
template.attr('id', `expression-${avatar}`);
|
||||
template.attr('data-avatar', avatar);
|
||||
template.find('.drag-grabber').attr('id', `expression-${avatar}header`);
|
||||
$('#visual-novel-wrapper').append(template);
|
||||
dragElement(template[0]);
|
||||
template.toggleClass('hidden', noSprites);
|
||||
setImage(template.find('img'), defaultSpritePath || '');
|
||||
const fadeInPromise = new Promise(resolve => {
|
||||
template.fadeIn(250, () => resolve());
|
||||
});
|
||||
createCharacterPromises.push(fadeInPromise);
|
||||
const setSpritePromise = setLastMessageSprite(template.find('img'), avatar, labels);
|
||||
setSpritePromises.push(setSpritePromise);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.allSettled(createCharacterPromises);
|
||||
return setSpritePromises;
|
||||
}
|
||||
|
||||
async function visualNovelUpdateLayers(container) {
|
||||
const context = getContext();
|
||||
const group = context.groups.find(x => x.id == context.groupId);
|
||||
const recentMessages = context.chat.map(x => x.original_avatar).filter(x => x).reverse().filter(onlyUnique);
|
||||
const filteredMembers = group.members.filter(x => !group.disabled_members.includes(x));
|
||||
const layerIndices = filteredMembers.slice().sort((a, b) => {
|
||||
const aRecentIndex = recentMessages.indexOf(a);
|
||||
const bRecentIndex = recentMessages.indexOf(b);
|
||||
const aFilteredIndex = filteredMembers.indexOf(a);
|
||||
const bFilteredIndex = filteredMembers.indexOf(b);
|
||||
|
||||
if (aRecentIndex !== -1 && bRecentIndex !== -1) {
|
||||
return bRecentIndex - aRecentIndex;
|
||||
} else if (aRecentIndex !== -1) {
|
||||
return 1;
|
||||
} else if (bRecentIndex !== -1) {
|
||||
return -1;
|
||||
} else {
|
||||
return aFilteredIndex - bFilteredIndex;
|
||||
}
|
||||
});
|
||||
|
||||
const setLayerIndicesPromises = [];
|
||||
|
||||
const sortFunction = (a, b) => {
|
||||
const avatarA = $(a).data('avatar');
|
||||
const avatarB = $(b).data('avatar');
|
||||
const indexA = filteredMembers.indexOf(avatarA);
|
||||
const indexB = filteredMembers.indexOf(avatarB);
|
||||
return indexA - indexB;
|
||||
};
|
||||
|
||||
const containerWidth = container.width();
|
||||
const pivotalPoint = containerWidth * 0.5;
|
||||
|
||||
let images = $('.expression-holder');
|
||||
let imagesWidth = [];
|
||||
|
||||
images.sort(sortFunction).each(function () {
|
||||
imagesWidth.push($(this).width());
|
||||
});
|
||||
|
||||
let totalWidth = imagesWidth.reduce((a, b) => a + b, 0);
|
||||
let currentPosition = pivotalPoint - (totalWidth / 2);
|
||||
|
||||
if (totalWidth > containerWidth) {
|
||||
let totalOverlap = totalWidth - containerWidth;
|
||||
let totalWidthWithoutWidest = imagesWidth.reduce((a, b) => a + b, 0) - Math.max(...imagesWidth);
|
||||
let overlaps = imagesWidth.map(width => (width / totalWidthWithoutWidest) * totalOverlap);
|
||||
imagesWidth = imagesWidth.map((width, index) => width - overlaps[index]);
|
||||
currentPosition = 0; // Reset the initial position to 0
|
||||
}
|
||||
|
||||
images.sort(sortFunction).each((index, current) => {
|
||||
const element = $(current);
|
||||
|
||||
// skip repositioning of dragged elements
|
||||
if (element.data('dragged')) {
|
||||
currentPosition += imagesWidth[index];
|
||||
return;
|
||||
}
|
||||
|
||||
const avatar = element.data('avatar');
|
||||
const layerIndex = layerIndices.indexOf(avatar);
|
||||
element.css('z-index', layerIndex);
|
||||
element.show();
|
||||
|
||||
const promise = new Promise(resolve => {
|
||||
element.animate({ left: currentPosition + 'px' }, 500, () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
currentPosition += imagesWidth[index];
|
||||
|
||||
setLayerIndicesPromises.push(promise);
|
||||
});
|
||||
|
||||
await Promise.allSettled(setLayerIndicesPromises);
|
||||
}
|
||||
|
||||
async function setLastMessageSprite(img, avatar, labels) {
|
||||
const context = getContext();
|
||||
const lastMessage = context.chat.slice().reverse().find(x => x.original_avatar == avatar || (x.force_avatar && x.force_avatar.includes(encodeURIComponent(avatar))));
|
||||
|
||||
if (lastMessage) {
|
||||
const text = lastMessage.mes || '';
|
||||
let spriteFolderName = lastMessage.name;
|
||||
const avatarFileName = getSpriteFolderName(lastMessage);
|
||||
const expressionOverride = extension_settings.expressionOverrides.find((e) =>
|
||||
e.name == avatarFileName
|
||||
);
|
||||
|
||||
if (expressionOverride && expressionOverride.path) {
|
||||
spriteFolderName = expressionOverride.path;
|
||||
}
|
||||
|
||||
const sprites = spriteCache[spriteFolderName] || [];
|
||||
const label = await getExpressionLabel(text);
|
||||
const path = labels.includes(label) ? sprites.find(x => x.label === label)?.path : '';
|
||||
|
||||
if (path) {
|
||||
setImage(img, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setImage(img, path) {
|
||||
img.attr('src', path);
|
||||
img.removeClass('default');
|
||||
img.off('error');
|
||||
img.on('error', function () {
|
||||
console.debug('Error loading image', path);
|
||||
$(this).off('error');
|
||||
$(this).attr('src', '');
|
||||
});
|
||||
}
|
||||
|
||||
function onExpressionsShowDefaultInput() {
|
||||
const value = $(this).prop('checked');
|
||||
extension_settings.expressions.showDefault = value;
|
||||
@ -58,24 +312,6 @@ function onExpressionsShowDefaultInput() {
|
||||
}
|
||||
}
|
||||
|
||||
let isWorkerBusy = false;
|
||||
|
||||
async function moduleWorkerWrapper() {
|
||||
// Don't touch me I'm busy...
|
||||
if (isWorkerBusy) {
|
||||
return;
|
||||
}
|
||||
|
||||
// I'm free. Let's update!
|
||||
try {
|
||||
isWorkerBusy = true;
|
||||
await moduleWorker();
|
||||
}
|
||||
finally {
|
||||
isWorkerBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function moduleWorker() {
|
||||
const context = getContext();
|
||||
|
||||
@ -91,11 +327,39 @@ async function moduleWorker() {
|
||||
spriteCache = {};
|
||||
}
|
||||
|
||||
const vnMode = isVisualNovelMode();
|
||||
const vnWrapperVisible = $('#visual-novel-wrapper').is(':visible');
|
||||
|
||||
if (vnMode) {
|
||||
$('#expression-wrapper').hide();
|
||||
$('#visual-novel-wrapper').show();
|
||||
} else {
|
||||
$('#expression-wrapper').show();
|
||||
$('#visual-novel-wrapper').hide();
|
||||
}
|
||||
|
||||
const vnStateChanged = vnMode !== vnWrapperVisible;
|
||||
|
||||
if (vnStateChanged) {
|
||||
lastMessage = null;
|
||||
$('#visual-novel-wrapper').empty();
|
||||
$("#expression-holder").css({ top: '', left: '', right: '', bottom: '', height: '', width: '', margin: '' });
|
||||
}
|
||||
|
||||
const currentLastMessage = getLastCharacterMessage();
|
||||
let spriteFolderName = currentLastMessage.name;
|
||||
const avatarFileName = getSpriteFolderName(currentLastMessage);
|
||||
const expressionOverride = extension_settings.expressionOverrides.find((e) =>
|
||||
e.name == avatarFileName
|
||||
);
|
||||
|
||||
if (expressionOverride && expressionOverride.path) {
|
||||
spriteFolderName = expressionOverride.path;
|
||||
}
|
||||
|
||||
// character has no expressions or it is not loaded
|
||||
if (Object.keys(spriteCache).length === 0) {
|
||||
await validateImages(currentLastMessage.name);
|
||||
await validateImages(spriteFolderName);
|
||||
lastCharacter = context.groupId || context.characterId;
|
||||
}
|
||||
|
||||
@ -106,7 +370,8 @@ async function moduleWorker() {
|
||||
lastCharacter = context.groupId || context.characterId;
|
||||
|
||||
if (context.groupId) {
|
||||
await validateImages(currentLastMessage.name, true);
|
||||
await validateImages(spriteFolderName, true);
|
||||
await forceUpdateVisualNovelMode();
|
||||
}
|
||||
|
||||
return;
|
||||
@ -117,13 +382,13 @@ async function moduleWorker() {
|
||||
expressionsList = null;
|
||||
spriteCache = {};
|
||||
expressionsList = await getExpressionsList();
|
||||
await validateImages(currentLastMessage.name, true);
|
||||
await validateImages(spriteFolderName, true);
|
||||
await forceUpdateVisualNovelMode();
|
||||
}
|
||||
|
||||
offlineMode.css('display', 'none');
|
||||
}
|
||||
|
||||
|
||||
// check if last message changed
|
||||
if ((lastCharacter === context.characterId || lastCharacter === context.groupId)
|
||||
&& lastMessage === currentLastMessage.mes) {
|
||||
@ -137,32 +402,22 @@ async function moduleWorker() {
|
||||
|
||||
try {
|
||||
inApiCall = true;
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/classify';
|
||||
let expression = await getExpressionLabel(currentLastMessage.mes);
|
||||
|
||||
const apiResult = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Bypass-Tunnel-Reminder': 'bypass',
|
||||
},
|
||||
body: JSON.stringify({ text: currentLastMessage.mes })
|
||||
});
|
||||
|
||||
if (apiResult.ok) {
|
||||
const name = context.groupId ? currentLastMessage.name : context.name2;
|
||||
const force = !!context.groupId;
|
||||
const data = await apiResult.json();
|
||||
let expression = data.classification[0].label;
|
||||
|
||||
// Character won't be angry on you for swiping
|
||||
if (currentLastMessage.mes == '...' && expressionsList.includes('joy')) {
|
||||
expression = 'joy';
|
||||
}
|
||||
|
||||
setExpression(name, expression, force);
|
||||
// If we're not already overriding the folder name, account for group chats.
|
||||
if (spriteFolderName === currentLastMessage.name && !context.groupId) {
|
||||
spriteFolderName = context.name2;
|
||||
}
|
||||
|
||||
const force = !!context.groupId;
|
||||
|
||||
// Character won't be angry on you for swiping
|
||||
if (currentLastMessage.mes == '...' && expressionsList.includes(FALLBACK_EXPRESSION)) {
|
||||
expression = FALLBACK_EXPRESSION;
|
||||
}
|
||||
|
||||
await sendExpressionCall(spriteFolderName, expression, force, vnMode);
|
||||
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error);
|
||||
@ -174,6 +429,61 @@ async function moduleWorker() {
|
||||
}
|
||||
}
|
||||
|
||||
function getSpriteFolderName(message) {
|
||||
const context = getContext();
|
||||
let avatarPath = '';
|
||||
|
||||
if (context.groupId) {
|
||||
avatarPath = message.original_avatar || context.characters.find(x => message.force_avatar && message.force_avatar.includes(encodeURIComponent(x.avatar)))?.avatar;
|
||||
}
|
||||
else if (context.characterId) {
|
||||
avatarPath = getCharaFilename();
|
||||
}
|
||||
|
||||
if (!avatarPath) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const folderName = avatarPath.replace(/\.[^/.]+$/, "");
|
||||
return folderName;
|
||||
}
|
||||
|
||||
async function sendExpressionCall(name, expression, force, vnMode) {
|
||||
if (!vnMode) {
|
||||
vnMode = isVisualNovelMode();
|
||||
}
|
||||
|
||||
if (vnMode) {
|
||||
await updateVisualNovelMode(name, expression);
|
||||
} else {
|
||||
setExpression(name, expression, force);
|
||||
}
|
||||
}
|
||||
|
||||
async function getExpressionLabel(text) {
|
||||
// Return if text is undefined, saving a costly fetch request
|
||||
if (!modules.includes('classify') || !text) {
|
||||
return FALLBACK_EXPRESSION;
|
||||
}
|
||||
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/classify';
|
||||
|
||||
const apiResult = await doExtrasFetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Bypass-Tunnel-Reminder': 'bypass',
|
||||
},
|
||||
body: JSON.stringify({ text: text }),
|
||||
});
|
||||
|
||||
if (apiResult.ok) {
|
||||
const data = await apiResult.json();
|
||||
return data.classification[0].label;
|
||||
}
|
||||
}
|
||||
|
||||
function getLastCharacterMessage() {
|
||||
const context = getContext();
|
||||
const reversedChat = context.chat.slice().reverse();
|
||||
@ -183,10 +493,10 @@ function getLastCharacterMessage() {
|
||||
continue;
|
||||
}
|
||||
|
||||
return { mes: mes.mes, name: mes.name };
|
||||
return { mes: mes.mes, name: mes.name, original_avatar: mes.original_avatar, force_avatar: mes.force_avatar };
|
||||
}
|
||||
|
||||
return { mes: '', name: null };
|
||||
return { mes: '', name: null, original_avatar: null, force_avatar: null };
|
||||
}
|
||||
|
||||
function removeExpression() {
|
||||
@ -206,7 +516,7 @@ async function validateImages(character, forceRedrawCached) {
|
||||
|
||||
if (spriteCache[character]) {
|
||||
if (forceRedrawCached && $('#image_list').data('name') !== character) {
|
||||
console.log('force redrawing character sprites list')
|
||||
console.debug('force redrawing character sprites list')
|
||||
drawSpritesList(character, labels, spriteCache[character]);
|
||||
}
|
||||
|
||||
@ -255,7 +565,7 @@ function getListItem(item, imageSrc, textClass) {
|
||||
}
|
||||
|
||||
async function getSpritesList(name) {
|
||||
console.log('getting sprites list');
|
||||
console.debug('getting sprites list');
|
||||
|
||||
try {
|
||||
const result = await fetch(`/get_sprites?name=${encodeURIComponent(name)}`);
|
||||
@ -283,7 +593,7 @@ async function getExpressionsList() {
|
||||
url.pathname = '/api/classify/labels';
|
||||
|
||||
try {
|
||||
const apiResult = await fetch(url, {
|
||||
const apiResult = await doExtrasFetch(url, {
|
||||
method: 'GET',
|
||||
headers: { 'Bypass-Tunnel-Reminder': 'bypass' },
|
||||
});
|
||||
@ -302,19 +612,40 @@ async function getExpressionsList() {
|
||||
}
|
||||
|
||||
async function setExpression(character, expression, force) {
|
||||
console.log('entered setExpressions');
|
||||
console.debug('entered setExpressions');
|
||||
await validateImages(character);
|
||||
const img = $('img.expression');
|
||||
|
||||
const sprite = (spriteCache[character] && spriteCache[character].find(x => x.label === expression));
|
||||
console.log('checking for expression images to show..');
|
||||
console.debug('checking for expression images to show..');
|
||||
if (sprite) {
|
||||
console.log('setting expression from character images folder');
|
||||
console.debug('setting expression from character images folder');
|
||||
|
||||
if (force && isVisualNovelMode()) {
|
||||
const context = getContext();
|
||||
const group = context.groups.find(x => x.id === context.groupId);
|
||||
|
||||
for (const member of group.members) {
|
||||
const groupMember = context.characters.find(x => x.avatar === member);
|
||||
|
||||
if (!groupMember) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (groupMember.name == character) {
|
||||
setImage($(`.expression-holder[data-avatar="${member}"] img`), sprite.path);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
img.attr('src', sprite.path);
|
||||
img.removeClass('default');
|
||||
img.off('error');
|
||||
img.on('error', function () {
|
||||
console.debug('Expression image error', sprite.path);
|
||||
$(this).attr('src', '');
|
||||
$(this).off('error');
|
||||
if (force && extension_settings.expressions.showDefault) {
|
||||
setDefault();
|
||||
}
|
||||
@ -326,7 +657,7 @@ async function setExpression(character, expression, force) {
|
||||
}
|
||||
|
||||
function setDefault() {
|
||||
console.log('setting default');
|
||||
console.debug('setting default');
|
||||
const defImgUrl = `/img/default-expressions/${expression}.png`;
|
||||
//console.log(defImgUrl);
|
||||
img.attr('src', defImgUrl);
|
||||
@ -402,6 +733,86 @@ async function onClickExpressionUpload(event) {
|
||||
.trigger('click');
|
||||
}
|
||||
|
||||
async function onClickExpressionOverrideButton() {
|
||||
const context = getContext();
|
||||
const currentLastMessage = getLastCharacterMessage();
|
||||
const avatarFileName = getSpriteFolderName(currentLastMessage);
|
||||
|
||||
// If the avatar name couldn't be found, abort.
|
||||
if (!avatarFileName) {
|
||||
console.debug(`Could not find filename for character with name ${currentLastMessage.name} and ID ${context.characterId}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const overridePath = $("#expression_override").val();
|
||||
const existingOverrideIndex = extension_settings.expressionOverrides.findIndex((e) =>
|
||||
e.name == avatarFileName
|
||||
);
|
||||
|
||||
// If the path is empty, delete the entry from overrides
|
||||
if (overridePath === undefined || overridePath.length === 0) {
|
||||
if (existingOverrideIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
extension_settings.expressionOverrides.splice(existingOverrideIndex, 1);
|
||||
console.debug(`Removed existing override for ${avatarFileName}`);
|
||||
} else {
|
||||
// Properly override objects and clear the sprite cache of the previously set names
|
||||
const existingOverride = extension_settings.expressionOverrides[existingOverrideIndex];
|
||||
if (existingOverride) {
|
||||
Object.assign(existingOverride, { path: overridePath });
|
||||
delete spriteCache[existingOverride.name];
|
||||
} else {
|
||||
const characterOverride = { name: avatarFileName, path: overridePath };
|
||||
extension_settings.expressionOverrides.push(characterOverride);
|
||||
delete spriteCache[currentLastMessage.name];
|
||||
}
|
||||
|
||||
console.debug(`Added/edited expression override for character with filename ${avatarFileName} to folder ${overridePath}`);
|
||||
}
|
||||
|
||||
saveSettingsDebounced();
|
||||
|
||||
// Refresh sprites list. Assume the override path has been properly handled.
|
||||
try {
|
||||
$('#visual-novel-wrapper').empty();
|
||||
await validateImages(overridePath.length === 0 ? currentLastMessage.name : overridePath, true);
|
||||
const expression = await getExpressionLabel(currentLastMessage.mes);
|
||||
await sendExpressionCall(overridePath.length === 0 ? currentLastMessage.name : overridePath, expression, true);
|
||||
forceUpdateVisualNovelMode();
|
||||
} catch (error) {
|
||||
console.debug(`Setting expression override for ${avatarFileName} failed with error: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function onClickExpressionOverrideRemoveAllButton() {
|
||||
// Remove all the overrided entries from sprite cache
|
||||
for (const element of extension_settings.expressionOverrides) {
|
||||
delete spriteCache[element.name];
|
||||
}
|
||||
|
||||
extension_settings.expressionOverrides = [];
|
||||
saveSettingsDebounced();
|
||||
|
||||
console.debug("All expression image overrides have been cleared.");
|
||||
|
||||
// Refresh sprites list to use the default name if applicable
|
||||
try {
|
||||
$('#visual-novel-wrapper').empty();
|
||||
const currentLastMessage = getLastCharacterMessage();
|
||||
await validateImages(currentLastMessage.name, true);
|
||||
const expression = await getExpressionLabel(currentLastMessage.mes);
|
||||
await sendExpressionCall(currentLastMessage.name, expression, true);
|
||||
forceUpdateVisualNovelMode();
|
||||
|
||||
console.debug(extension_settings.expressionOverrides);
|
||||
} catch (error) {
|
||||
console.debug(`The current expression could not be set because of error: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function onClickExpressionUploadPackButton() {
|
||||
const name = $('#image_list').data('name');
|
||||
|
||||
@ -457,6 +868,28 @@ async function onClickExpressionDelete(event) {
|
||||
await validateImages(name);
|
||||
}
|
||||
|
||||
function setExpressionOverrideHtml(forceClear = false) {
|
||||
const currentLastMessage = getLastCharacterMessage();
|
||||
const avatarFileName = getSpriteFolderName(currentLastMessage);
|
||||
if (!avatarFileName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const expressionOverride = extension_settings.expressionOverrides.find((e) =>
|
||||
e.name == avatarFileName
|
||||
);
|
||||
|
||||
if (expressionOverride && expressionOverride.path) {
|
||||
$("#expression_override").val(expressionOverride.path);
|
||||
} else if (expressionOverride) {
|
||||
delete extension_settings.expressionOverrides[expressionOverride.name];
|
||||
}
|
||||
|
||||
if (forceClear && !expressionOverride) {
|
||||
$("#expression_override").val("");
|
||||
}
|
||||
}
|
||||
|
||||
(function () {
|
||||
function addExpressionImage() {
|
||||
const html = `
|
||||
@ -468,23 +901,41 @@ async function onClickExpressionDelete(event) {
|
||||
</div>`;
|
||||
$('body').append(html);
|
||||
}
|
||||
function addVisualNovelMode() {
|
||||
const html = `
|
||||
<div id="visual-novel-wrapper">
|
||||
</div>`
|
||||
const element = $(html);
|
||||
element.hide();
|
||||
$('body').append(element);
|
||||
}
|
||||
function addSettings() {
|
||||
|
||||
const html = `
|
||||
<div class="expression_settings">
|
||||
<div class="inline-drawer">
|
||||
<div class="inline-drawer-toggle inline-drawer-header">
|
||||
<b>Expression images</b>
|
||||
<b>Character Expressions</b>
|
||||
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
||||
</div>
|
||||
<div class="inline-drawer-content">
|
||||
<p class="offline_mode">You are in offline mode. Click on the image below to set the expression.</p>
|
||||
<div class="offline_mode">
|
||||
<small>You are in offline mode. Click on the image below to set the expression.</small>
|
||||
</div>
|
||||
<div class="flex-container flexnowrap">
|
||||
<input id="expression_override" type="text" class="text_pole" placeholder="Override folder name" />
|
||||
<input id="expression_override_button" class="menu_button" type="submit" value="Submit" />
|
||||
</div>
|
||||
<div id="image_list"></div>
|
||||
<div class="expression_buttons">
|
||||
<div class="expression_buttons flex-container spaceEvenly">
|
||||
<div id="expression_upload_pack_button" class="menu_button">
|
||||
<i class="fa-solid fa-file-zipper"></i>
|
||||
<span>Upload sprite pack (ZIP)</span>
|
||||
</div>
|
||||
<div id="expression_override_cleanup_button" class="menu_button">
|
||||
<i class="fa-solid fa-trash-can"></i>
|
||||
<span>Remove all image overrides</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint"><b>Hint:</b> <i>Create new folder in the <b>public/characters/</b> folder and name it as the name of the character.
|
||||
Put images with expressions there. File names should follow the pattern: <tt>[expression_label].[image_format]</tt></i></p>
|
||||
@ -498,17 +949,32 @@ async function onClickExpressionDelete(event) {
|
||||
</div>
|
||||
`;
|
||||
$('#extensions_settings').append(html);
|
||||
$('#expression_override_button').on('click', onClickExpressionOverrideButton);
|
||||
$('#expressions_show_default').on('input', onExpressionsShowDefaultInput);
|
||||
$('#expression_upload_pack_button').on('click', onClickExpressionUploadPackButton);
|
||||
$('#expressions_show_default').prop('checked', extension_settings.expressions.showDefault).trigger('input');
|
||||
$('#expression_override_cleanup_button').on('click', onClickExpressionOverrideRemoveAllButton);
|
||||
$(document).on('click', '.expression_list_item', onClickExpressionImage);
|
||||
$(document).on('click', '.expression_list_upload', onClickExpressionUpload);
|
||||
$(document).on('click', '.expression_list_delete', onClickExpressionDelete);
|
||||
$(window).on("resize", updateVisualNovelModeDebounced);
|
||||
$('.expression_settings').hide();
|
||||
}
|
||||
|
||||
addExpressionImage();
|
||||
addVisualNovelMode();
|
||||
addSettings();
|
||||
setInterval(moduleWorkerWrapper, UPDATE_INTERVAL);
|
||||
moduleWorkerWrapper();
|
||||
const wrapper = new ModuleWorkerWrapper(moduleWorker);
|
||||
const updateFunction = wrapper.update.bind(wrapper);
|
||||
setInterval(updateFunction, UPDATE_INTERVAL);
|
||||
moduleWorker();
|
||||
eventSource.on(event_types.CHAT_CHANGED, () => {
|
||||
setExpressionOverrideHtml();
|
||||
|
||||
if (isVisualNovelMode()) {
|
||||
$('#visual-novel-wrapper').empty();
|
||||
}
|
||||
});
|
||||
eventSource.on(event_types.MOVABLE_PANELS_RESET, updateVisualNovelModeDebounced);
|
||||
eventSource.on(event_types.GROUP_UPDATED, updateVisualNovelModeDebounced);
|
||||
})();
|
||||
|
@ -9,5 +9,5 @@
|
||||
"css": "style.css",
|
||||
"author": "Cohee#1207",
|
||||
"version": "1.0.0",
|
||||
"homePage": "https://github.com/Cohee1207/SillyTavern"
|
||||
"homePage": "https://github.com/SillyTavern/SillyTavern"
|
||||
}
|
@ -10,6 +10,27 @@
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
#visual-novel-wrapper {
|
||||
display: flex;
|
||||
height: calc(100vh - 40px);
|
||||
width: 100vw;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#visual-novel-wrapper .expression-holder {
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
#visual-novel-wrapper .hidden {
|
||||
display: none !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
/*#visual-novel-wrapper img.expression {
|
||||
object-fit: cover;
|
||||
}*/
|
||||
|
||||
.expression-holder {
|
||||
min-width: 100px;
|
||||
min-height: 100px;
|
||||
@ -54,6 +75,7 @@ img.expression.default {
|
||||
.expression_list_item {
|
||||
position: relative;
|
||||
max-width: 20%;
|
||||
min-width: 100px;
|
||||
max-height: 200px;
|
||||
background-color: #515151b0;
|
||||
border-radius: 10px;
|
||||
@ -87,10 +109,16 @@ img.expression.default {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 20%;
|
||||
/* height: 20%; */
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.menu_button.expression_list_delete,
|
||||
.menu_button.expression_list_upload {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.expression_list_image {
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
@ -115,11 +143,6 @@ img.expression.default {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.expression_settings p {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.expression_settings label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -1,11 +1,17 @@
|
||||
import { chat_metadata, saveSettingsDebounced } from "../../../script.js";
|
||||
import { extension_settings, getContext } from "../../extensions.js";
|
||||
import {
|
||||
chat_metadata,
|
||||
eventSource,
|
||||
event_types,
|
||||
getTokenCount,
|
||||
saveSettingsDebounced,
|
||||
this_chid,
|
||||
} from "../../../script.js";
|
||||
import { selected_group } from "../../group-chats.js";
|
||||
import { ModuleWorkerWrapper, extension_settings, getContext, saveMetadataDebounced } from "../../extensions.js";
|
||||
import { registerSlashCommand } from "../../slash-commands.js";
|
||||
import { debounce } from "../../utils.js";
|
||||
import { getCharaFilename, debounce } from "../../utils.js";
|
||||
export { MODULE_NAME };
|
||||
|
||||
const saveMetadataDebounced = debounce(async () => await getContext().saveMetadata(), 1000);
|
||||
|
||||
const MODULE_NAME = '2_floating_prompt'; // <= Deliberate, for sorting lower than memory
|
||||
const UPDATE_INTERVAL = 1000;
|
||||
|
||||
@ -66,8 +72,13 @@ function setNotePositionCommand(_, text) {
|
||||
toastr.info("Author's Note position updated");
|
||||
}
|
||||
|
||||
const setMainPromptTokenCounterDebounced = debounce((value) => $('#extension_floating_prompt_token_counter').text(getTokenCount(value)), 1000);
|
||||
const setCharaPromptTokenCounterDebounced = debounce((value) => $('#extension_floating_chara_token_counter').text(getTokenCount(value)), 1000);
|
||||
const setDefaultPromptTokenCounterDebounced = debounce((value) => $('#extension_floating_default_token_counter').text(getTokenCount(value)), 1000);
|
||||
|
||||
async function onExtensionFloatingPromptInput() {
|
||||
chat_metadata[metadata_keys.prompt] = $(this).val();
|
||||
setMainPromptTokenCounterDebounced(chat_metadata[metadata_keys.prompt]);
|
||||
saveMetadataDebounced();
|
||||
}
|
||||
|
||||
@ -93,8 +104,66 @@ async function onExtensionFloatingPositionInput(e) {
|
||||
saveMetadataDebounced();
|
||||
}
|
||||
|
||||
function onExtensionFloatingCharaPromptInput() {
|
||||
const tempPrompt = $(this).val();
|
||||
const avatarName = getCharaFilename();
|
||||
let tempCharaNote = {
|
||||
name: avatarName,
|
||||
prompt: tempPrompt
|
||||
}
|
||||
|
||||
setCharaPromptTokenCounterDebounced(tempPrompt);
|
||||
|
||||
let existingCharaNoteIndex;
|
||||
let existingCharaNote;
|
||||
|
||||
if (extension_settings.note.chara) {
|
||||
existingCharaNoteIndex = extension_settings.note.chara.findIndex((e) => e.name === avatarName);
|
||||
existingCharaNote = extension_settings.note.chara[existingCharaNoteIndex]
|
||||
}
|
||||
|
||||
if (tempPrompt.length === 0 &&
|
||||
extension_settings.note.chara &&
|
||||
existingCharaNote &&
|
||||
!existingCharaNote.useChara
|
||||
) {
|
||||
extension_settings.note.chara.splice(existingCharaNoteIndex, 1);
|
||||
}
|
||||
else if (extension_settings.note.chara && existingCharaNote) {
|
||||
Object.assign(existingCharaNote, tempCharaNote);
|
||||
}
|
||||
else if (avatarName && tempPrompt.length > 0) {
|
||||
if (!extension_settings.note.chara) {
|
||||
extension_settings.note.chara = []
|
||||
}
|
||||
Object.assign(tempCharaNote, { useChara: false })
|
||||
|
||||
extension_settings.note.chara.push(tempCharaNote);
|
||||
} else {
|
||||
console.log("Character author's note error: No avatar name key could be found.");
|
||||
toastr.error("Something went wrong. Could not save character's author's note.");
|
||||
|
||||
// Don't save settings if something went wrong
|
||||
return;
|
||||
}
|
||||
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onExtensionFloatingCharaCheckboxChanged() {
|
||||
const value = !!$(this).prop('checked');
|
||||
const charaNote = extension_settings.note.chara.find((e) => e.name === getCharaFilename());
|
||||
|
||||
if (charaNote) {
|
||||
charaNote.useChara = value;
|
||||
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
}
|
||||
|
||||
function onExtensionFloatingDefaultInput() {
|
||||
extension_settings.note.default = $(this).val();
|
||||
setDefaultPromptTokenCounterDebounced(extension_settings.note.default);
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
@ -107,27 +176,17 @@ function loadSettings() {
|
||||
$('#extension_floating_interval').val(chat_metadata[metadata_keys.interval]);
|
||||
$('#extension_floating_depth').val(chat_metadata[metadata_keys.depth]);
|
||||
$(`input[name="extension_floating_position"][value="${chat_metadata[metadata_keys.position]}"]`).prop('checked', true);
|
||||
|
||||
if (extension_settings.note.chara) {
|
||||
const charaNote = extension_settings.note.chara.find((e) => e.name === getCharaFilename());
|
||||
|
||||
$('#extension_floating_chara').val(charaNote ? charaNote.prompt : '');
|
||||
$('#extension_use_floating_chara').prop('checked', charaNote ? charaNote.useChara : false);
|
||||
}
|
||||
|
||||
$('#extension_floating_default').val(extension_settings.note.default);
|
||||
}
|
||||
|
||||
let isWorkerBusy = false;
|
||||
|
||||
async function moduleWorkerWrapper() {
|
||||
// Don't touch me I'm busy...
|
||||
if (isWorkerBusy) {
|
||||
return;
|
||||
}
|
||||
|
||||
// I'm free. Let's update!
|
||||
try {
|
||||
isWorkerBusy = true;
|
||||
await moduleWorker();
|
||||
}
|
||||
finally {
|
||||
isWorkerBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function moduleWorker() {
|
||||
const context = getContext();
|
||||
|
||||
@ -140,8 +199,8 @@ async function moduleWorker() {
|
||||
// take the count of messages
|
||||
let lastMessageNumber = Array.isArray(context.chat) && context.chat.length ? context.chat.filter(m => m.is_user).length : 0;
|
||||
|
||||
// special case for new chat
|
||||
if (Array.isArray(context.chat) && context.chat.length === 1) {
|
||||
// interval 1 should be inserted no matter what
|
||||
if (chat_metadata[metadata_keys.interval] === 1) {
|
||||
lastMessageNumber = 1;
|
||||
}
|
||||
|
||||
@ -155,21 +214,89 @@ async function moduleWorker() {
|
||||
? (lastMessageNumber % chat_metadata[metadata_keys.interval])
|
||||
: (chat_metadata[metadata_keys.interval] - lastMessageNumber);
|
||||
const shouldAddPrompt = messagesTillInsertion == 0;
|
||||
const prompt = shouldAddPrompt ? $('#extension_floating_prompt').val() : '';
|
||||
|
||||
let prompt = shouldAddPrompt ? $('#extension_floating_prompt').val() : '';
|
||||
if (shouldAddPrompt && extension_settings.note.chara) {
|
||||
const charaNote = extension_settings.note.chara.find((e) => e.name === getCharaFilename());
|
||||
|
||||
// Only replace with the chara note if the user checked the box
|
||||
if (charaNote && charaNote.useChara) {
|
||||
prompt = charaNote.prompt;
|
||||
}
|
||||
}
|
||||
|
||||
context.setExtensionPrompt(MODULE_NAME, prompt, chat_metadata[metadata_keys.position], chat_metadata[metadata_keys.depth]);
|
||||
$('#extension_floating_counter').text(shouldAddPrompt ? '0' : messagesTillInsertion);
|
||||
}
|
||||
|
||||
function onANMenuItemClick() {
|
||||
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,
|
||||
});
|
||||
|
||||
if ($("#ANBlockToggle")
|
||||
.siblings('.inline-drawer-content')
|
||||
.css('display') !== 'block') {
|
||||
$("#ANBlockToggle").click();
|
||||
}
|
||||
} else {
|
||||
$("#floatingPrompt").transition({
|
||||
opacity: 0.0,
|
||||
duration: 250,
|
||||
});
|
||||
setTimeout(function () {
|
||||
$("#floatingPrompt").hide();
|
||||
}, 250);
|
||||
|
||||
}
|
||||
//duplicate options menu close handler from script.js
|
||||
//because this listener takes priority
|
||||
$("#options").stop().fadeOut(250);
|
||||
} else {
|
||||
toastr.warning(`Select a character before trying to use Author's Note`, '', { timeOut: 2000 });
|
||||
}
|
||||
}
|
||||
|
||||
function onChatChanged() {
|
||||
const tokenCounter1 = chat_metadata[metadata_keys.prompt] ? getTokenCount(chat_metadata[metadata_keys.prompt]) : 0;
|
||||
$('#extension_floating_prompt_token_counter').text(tokenCounter1);
|
||||
|
||||
let tokenCounter2;
|
||||
if (extension_settings.note.chara) {
|
||||
const charaNote = extension_settings.note.chara.find((e) => e.name === getCharaFilename());
|
||||
|
||||
if (charaNote) {
|
||||
tokenCounter2 = getTokenCount(charaNote.prompt);
|
||||
}
|
||||
}
|
||||
|
||||
if (tokenCounter2) {
|
||||
$('#extension_floating_chara_token_counter').text(tokenCounter2);
|
||||
}
|
||||
|
||||
const tokenCounter3 = extension_settings.note.default ? getTokenCount(extension_settings.note.default) : 0;
|
||||
$('#extension_floating_default_token_counter').text(tokenCounter3);
|
||||
}
|
||||
|
||||
(function () {
|
||||
function addExtensionsSettings() {
|
||||
const settingsHtml = `
|
||||
<div id="floatingPrompt" class="drawer-content flexGap5">
|
||||
<div id="floatingPromptheader" class="fa-solid fa-grip drag-grabber"></div>
|
||||
<div class="panelControlBar flex-container">
|
||||
<div id="floatingPromptheader" class="fa-solid fa-grip drag-grabber"></div>
|
||||
<div id="ANClose" class="fa-solid fa-circle-xmark"></div>
|
||||
</div>
|
||||
<div name="floatingPromptHolder">
|
||||
<div class="inline-drawer">
|
||||
<div id="ANBlockToggle" class="inline-drawer-toggle inline-drawer-header">
|
||||
<b>Author's Note</b>
|
||||
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
||||
|
||||
</div>
|
||||
<div class="inline-drawer-content">
|
||||
<small>
|
||||
@ -178,6 +305,7 @@ async function moduleWorker() {
|
||||
</small>
|
||||
|
||||
<textarea id="extension_floating_prompt" class="text_pole" rows="8" maxlength="10000"></textarea>
|
||||
<div class="extension_token_counter">Tokens: <span id="extension_floating_prompt_token_counter">0</small></div>
|
||||
|
||||
<div class="floating_prompt_radio_group">
|
||||
<label>
|
||||
@ -193,7 +321,7 @@ async function moduleWorker() {
|
||||
|
||||
<label for="extension_floating_interval">Insertion Frequency</label>
|
||||
|
||||
<input id="extension_floating_interval" class="text_pole widthUnset" type="number" min="0" max="999" /><small> (0 = Disable)</small>
|
||||
<input id="extension_floating_interval" class="text_pole widthUnset" type="number" min="0" max="999" /><small> (0 = Disable, 1 = Always)</small>
|
||||
<br>
|
||||
|
||||
<span>User inputs until next insertion: <span id="extension_floating_counter">(disabled)</span></span>
|
||||
@ -201,6 +329,25 @@ async function moduleWorker() {
|
||||
</div>
|
||||
</div>
|
||||
<hr class="sysHR">
|
||||
<div class="inline-drawer">
|
||||
<div id="charaANBlockToggle" class="inline-drawer-toggle inline-drawer-header">
|
||||
<b>Character Author's Note</b>
|
||||
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
||||
</div>
|
||||
<div class="inline-drawer-content">
|
||||
<small>Will be automatically added as the author's note for this character.</small>
|
||||
|
||||
<textarea id="extension_floating_chara" class="text_pole" rows="8" maxlength="10000"
|
||||
placeholder="Example:\n[Scenario: wacky adventures; Genre: romantic comedy; Style: verbose, creative]"></textarea>
|
||||
<div class="extension_token_counter">Tokens: <span id="extension_floating_chara_token_counter">0</small></div>
|
||||
|
||||
<label for="extension_use_floating_chara">
|
||||
<input id="extension_use_floating_chara" type="checkbox" />
|
||||
<span data-i18n="Use character author's note">Use character author's note</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="sysHR">
|
||||
<div class="inline-drawer">
|
||||
<div id="defaultANBlockToggle" class="inline-drawer-toggle inline-drawer-header">
|
||||
<b>Default Author's Note</b>
|
||||
@ -211,24 +358,45 @@ async function moduleWorker() {
|
||||
|
||||
<textarea id="extension_floating_default" class="text_pole" rows="8" maxlength="10000"
|
||||
placeholder="Example:\n[Scenario: wacky adventures; Genre: romantic comedy; Style: verbose, creative]"></textarea>
|
||||
<div class="extension_token_counter">Tokens: <span id="extension_floating_default_token_counter">0</small></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const ANButtonHtml = `
|
||||
<a id="option_toggle_AN">
|
||||
<i class="fa-lg fa-solid fa-note-sticky"></i>
|
||||
<span data-i18n="Author's Note">Author's Note</span>
|
||||
</a>
|
||||
`;
|
||||
$('#options .options-content').prepend(ANButtonHtml);
|
||||
$('#movingDivs').append(settingsHtml);
|
||||
$('#extension_floating_prompt').on('input', onExtensionFloatingPromptInput);
|
||||
$('#extension_floating_interval').on('input', onExtensionFloatingIntervalInput);
|
||||
$('#extension_floating_depth').on('input', onExtensionFloatingDepthInput);
|
||||
$('#extension_floating_chara').on('input', onExtensionFloatingCharaPromptInput);
|
||||
$('#extension_use_floating_chara').on('input', onExtensionFloatingCharaCheckboxChanged);
|
||||
$('#extension_floating_default').on('input', onExtensionFloatingDefaultInput);
|
||||
$('input[name="extension_floating_position"]').on('change', onExtensionFloatingPositionInput);
|
||||
$('#ANClose').on('click', function () {
|
||||
$("#floatingPrompt").transition({
|
||||
opacity: 0,
|
||||
duration: 200,
|
||||
easing: 'ease-in-out',
|
||||
});
|
||||
setTimeout(function () { $('#floatingPrompt').hide() }, 200);
|
||||
})
|
||||
$("#option_toggle_AN").on('click', onANMenuItemClick);
|
||||
}
|
||||
|
||||
addExtensionsSettings();
|
||||
setInterval(moduleWorkerWrapper, UPDATE_INTERVAL);
|
||||
const wrapper = new ModuleWorkerWrapper(moduleWorker);
|
||||
setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL);
|
||||
registerSlashCommand('note', setNoteTextCommand, [], "<span class='monospace'>(text)</span> – sets an author's note for the currently selected chat", true, true);
|
||||
registerSlashCommand('depth', setNoteDepthCommand, [], "<span class='monospace'>(number)</span> – sets an author's note depth for in-chat positioning", true, true);
|
||||
registerSlashCommand('freq', setNoteIntervalCommand, ['interval'], "<span class='monospace'>(number)</span> – sets an author's note insertion frequency", true, true);
|
||||
registerSlashCommand('pos', setNotePositionCommand, ['position'], "(<span class='monospace'>chat</span> or <span class='monospace'>scenario</span>) – sets an author's note position", true, true);
|
||||
eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
|
||||
})();
|
@ -7,5 +7,5 @@
|
||||
"css": "style.css",
|
||||
"author": "Cohee#1207",
|
||||
"version": "1.0.0",
|
||||
"homePage": "https://github.com/Cohee1207/SillyTavern"
|
||||
"homePage": "https://github.com/SillyTavern/SillyTavern"
|
||||
}
|
@ -8,6 +8,7 @@
|
||||
border: 1px solid var(--white30a);
|
||||
position: fixed;
|
||||
padding: 10px;
|
||||
padding-top: 25px;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 0 10px var(--black70a);
|
||||
@ -30,7 +31,37 @@
|
||||
color: orange;
|
||||
}
|
||||
|
||||
.extension_token_counter {
|
||||
font-size: calc(var(--mainFontSize) * 0.9);
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.floating_prompt_settings textarea {
|
||||
font-size: calc(var(--mainFontSize) * 0.9);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
#ANClose {
|
||||
height: 15px;
|
||||
aspect-ratio: 1 / 1;
|
||||
font-size: 20px;
|
||||
opacity: 0.5;
|
||||
transition: all 250ms;
|
||||
}
|
||||
|
||||
#ANClose:hover {
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.panelControlBar {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
#floatingPrompt .drag-grabber {
|
||||
position: unset;
|
||||
}
|
||||
|
@ -1,22 +1,23 @@
|
||||
import { saveSettingsDebounced, getCurrentChatId, system_message_types, eventSource, event_types } from "../../../script.js";
|
||||
import { humanizedDateTime } from "../../RossAscends-mods.js";
|
||||
import { getApiUrl, extension_settings, getContext } from "../../extensions.js";
|
||||
import { getFileText, onlyUnique, splitRecursive } from "../../utils.js";
|
||||
import { getApiUrl, extension_settings, getContext, doExtrasFetch } from "../../extensions.js";
|
||||
import { getFileText, onlyUnique, splitRecursive, IndexedDBStore } from "../../utils.js";
|
||||
export { MODULE_NAME };
|
||||
|
||||
const MODULE_NAME = 'chromadb';
|
||||
const dbStore = new IndexedDBStore('SillyTavern', MODULE_NAME);
|
||||
|
||||
const defaultSettings = {
|
||||
strategy: 'original',
|
||||
|
||||
keep_context: 10,
|
||||
keep_context_min: 1,
|
||||
keep_context_max: 100,
|
||||
keep_context_max: 500,
|
||||
keep_context_step: 1,
|
||||
|
||||
n_results: 20,
|
||||
n_results_min: 0,
|
||||
n_results_max: 100,
|
||||
n_results_max: 500,
|
||||
n_results_step: 1,
|
||||
|
||||
split_length: 384,
|
||||
@ -35,22 +36,21 @@ const postHeaders = {
|
||||
'Bypass-Tunnel-Reminder': 'bypass',
|
||||
};
|
||||
|
||||
const chatStateFlags = {};
|
||||
|
||||
function invalidateMessageSyncState(messageId) {
|
||||
async function invalidateMessageSyncState(messageId) {
|
||||
console.log('CHROMADB: invalidating message sync state', messageId);
|
||||
const state = getChatSyncState();
|
||||
state[messageId] = false;
|
||||
const state = await getChatSyncState();
|
||||
state[messageId] = 0;
|
||||
await dbStore.put(getCurrentChatId(), state);
|
||||
}
|
||||
|
||||
function getChatSyncState() {
|
||||
async function getChatSyncState() {
|
||||
const currentChatId = getCurrentChatId();
|
||||
if (!checkChatId(currentChatId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const context = getContext();
|
||||
const chatState = chatStateFlags[currentChatId] || [];
|
||||
const chatState = (await dbStore.get(currentChatId)) || [];
|
||||
|
||||
// if the chat length has decreased, it means that some messages were deleted
|
||||
if (chatState.length > context.chat.length) {
|
||||
@ -70,10 +70,10 @@ function getChatSyncState() {
|
||||
chatState.length = context.chat.length;
|
||||
for (let i = 0; i < chatState.length; i++) {
|
||||
if (chatState[i] === undefined) {
|
||||
chatState[i] = false;
|
||||
chatState[i] = 0;
|
||||
}
|
||||
}
|
||||
chatStateFlags[currentChatId] = chatState;
|
||||
await dbStore.put(currentChatId, chatState);
|
||||
|
||||
return chatState;
|
||||
}
|
||||
@ -83,7 +83,7 @@ async function loadSettings() {
|
||||
Object.assign(extension_settings.chromadb, defaultSettings);
|
||||
}
|
||||
|
||||
console.log(`loading chromadb strat:${extension_settings.chromadb.strategy}`);
|
||||
console.debug(`loading chromadb strat:${extension_settings.chromadb.strategy}`);
|
||||
$("#chromadb_strategy option[value=" + extension_settings.chromadb.strategy + "]").attr(
|
||||
"selected",
|
||||
"true"
|
||||
@ -96,7 +96,7 @@ async function loadSettings() {
|
||||
}
|
||||
|
||||
function onStrategyChange() {
|
||||
console.log('changing chromadb strat');
|
||||
console.debug('changing chromadb strat');
|
||||
extension_settings.chromadb.strategy = $('#chromadb_strategy').val();
|
||||
|
||||
//$('#chromadb_strategy').select(extension_settings.chromadb.strategy);
|
||||
@ -144,12 +144,12 @@ async function addMessages(chat_id, messages) {
|
||||
url.pathname = '/api/chromadb';
|
||||
|
||||
const messagesDeepCopy = JSON.parse(JSON.stringify(messages));
|
||||
let splittedMessages = [];
|
||||
let splitMessages = [];
|
||||
|
||||
let id = 0;
|
||||
messagesDeepCopy.forEach((m, index) => {
|
||||
const split = splitRecursive(m.mes, extension_settings.chromadb.split_length);
|
||||
splittedMessages.push(...split.map(text => ({
|
||||
splitMessages.push(...split.map(text => ({
|
||||
...m,
|
||||
mes: text,
|
||||
send_date: id,
|
||||
@ -159,14 +159,14 @@ async function addMessages(chat_id, messages) {
|
||||
})));
|
||||
});
|
||||
|
||||
splittedMessages = filterSyncedMessages(splittedMessages);
|
||||
splitMessages = await filterSyncedMessages(splitMessages);
|
||||
|
||||
// no messages to add
|
||||
if (splittedMessages.length === 0) {
|
||||
if (splitMessages.length === 0) {
|
||||
return { count: 0 };
|
||||
}
|
||||
|
||||
const transformedMessages = splittedMessages.map((m) => ({
|
||||
const transformedMessages = splitMessages.map((m) => ({
|
||||
id: m.id,
|
||||
role: m.is_user ? 'user' : 'assistant',
|
||||
content: m.mes,
|
||||
@ -174,7 +174,7 @@ async function addMessages(chat_id, messages) {
|
||||
meta: JSON.stringify(m),
|
||||
}));
|
||||
|
||||
const addMessagesResult = await fetch(url, {
|
||||
const addMessagesResult = await doExtrasFetch(url, {
|
||||
method: 'POST',
|
||||
headers: postHeaders,
|
||||
body: JSON.stringify({ chat_id, messages: transformedMessages }),
|
||||
@ -182,19 +182,18 @@ async function addMessages(chat_id, messages) {
|
||||
|
||||
if (addMessagesResult.ok) {
|
||||
const addMessagesData = await addMessagesResult.json();
|
||||
|
||||
return addMessagesData; // { count: 1 }
|
||||
}
|
||||
|
||||
return { count: 0 };
|
||||
}
|
||||
|
||||
function filterSyncedMessages(splittedMessages) {
|
||||
const syncState = getChatSyncState();
|
||||
async function filterSyncedMessages(splitMessages) {
|
||||
const syncState = await getChatSyncState();
|
||||
const removeIndices = [];
|
||||
const syncedIndices = [];
|
||||
for (let i = 0; i < splittedMessages.length; i++) {
|
||||
const index = splittedMessages[i].index;
|
||||
for (let i = 0; i < splitMessages.length; i++) {
|
||||
const index = splitMessages[i].index;
|
||||
|
||||
if (syncState[index]) {
|
||||
removeIndices.push(i);
|
||||
@ -205,19 +204,14 @@ function filterSyncedMessages(splittedMessages) {
|
||||
}
|
||||
|
||||
for (const index of syncedIndices) {
|
||||
syncState[index] = true;
|
||||
syncState[index] = 1;
|
||||
}
|
||||
|
||||
logSyncState(syncState);
|
||||
console.debug('CHROMADB: sync state', syncState.map((v, i) => ({ id: i, synced: v })));
|
||||
await dbStore.put(getCurrentChatId(), syncState);
|
||||
|
||||
// remove messages that are already synced
|
||||
return splittedMessages.filter((_, i) => !removeIndices.includes(i));
|
||||
}
|
||||
|
||||
function logSyncState(syncState) {
|
||||
const chat = getContext().chat;
|
||||
console.log('CHROMADB: sync state');
|
||||
console.table(syncState.map((v, i) => ({ synced: v, name: chat[i].name, message: chat[i].mes })));
|
||||
return splitMessages.filter((_, i) => !removeIndices.includes(i));
|
||||
}
|
||||
|
||||
async function onPurgeClick() {
|
||||
@ -228,14 +222,14 @@ async function onPurgeClick() {
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/chromadb/purge';
|
||||
|
||||
const purgeResult = await fetch(url, {
|
||||
const purgeResult = await doExtrasFetch(url, {
|
||||
method: 'POST',
|
||||
headers: postHeaders,
|
||||
body: JSON.stringify({ chat_id }),
|
||||
});
|
||||
|
||||
if (purgeResult.ok) {
|
||||
delete chatStateFlags[chat_id];
|
||||
await dbStore.delete(chat_id);
|
||||
toastr.success('ChromaDB context has been successfully cleared');
|
||||
}
|
||||
}
|
||||
@ -248,7 +242,7 @@ async function onExportClick() {
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/chromadb/export';
|
||||
|
||||
const exportResult = await fetch(url, {
|
||||
const exportResult = await doExtrasFetch(url, {
|
||||
method: 'POST',
|
||||
headers: postHeaders,
|
||||
body: JSON.stringify({ chat_id: currentChatId }),
|
||||
@ -291,7 +285,7 @@ async function onSelectImportFile(e) {
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/chromadb/import';
|
||||
|
||||
const importResult = await fetch(url, {
|
||||
const importResult = await doExtrasFetch(url, {
|
||||
method: 'POST',
|
||||
headers: postHeaders,
|
||||
body: JSON.stringify(imported),
|
||||
@ -319,7 +313,7 @@ async function queryMessages(chat_id, query) {
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/chromadb/query';
|
||||
|
||||
const queryMessagesResult = await fetch(url, {
|
||||
const queryMessagesResult = await doExtrasFetch(url, {
|
||||
method: 'POST',
|
||||
headers: postHeaders,
|
||||
body: JSON.stringify({ chat_id, query, n_results: extension_settings.chromadb.n_results }),
|
||||
@ -372,7 +366,7 @@ async function onSelectInjectFile(e) {
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/chromadb';
|
||||
|
||||
const addMessagesResult = await fetch(url, {
|
||||
const addMessagesResult = await doExtrasFetch(url, {
|
||||
method: 'POST',
|
||||
headers: postHeaders,
|
||||
body: JSON.stringify({ chat_id: currentChatId, messages: messages }),
|
||||
@ -427,7 +421,7 @@ window.chromadb_interceptGeneration = async (chat) => {
|
||||
send_date: 0,
|
||||
}
|
||||
);
|
||||
newChat.push(...queriedMessages.map(m => JSON.parse(m.meta)));
|
||||
newChat.push(...queriedMessages.map(m => m.meta).filter(onlyUnique).map(JSON.parse));
|
||||
newChat.push(
|
||||
{
|
||||
is_name: false,
|
||||
@ -443,7 +437,7 @@ window.chromadb_interceptGeneration = async (chat) => {
|
||||
if (selectedStrategy === 'original') {
|
||||
//removes .length # messages from the start of 'kept messages'
|
||||
//replaces them with chromaDB results (with no separator)
|
||||
newChat.push(...queriedMessages.map(m => JSON.parse(m.meta)));
|
||||
newChat.push(...queriedMessages.map(m => m.meta).filter(onlyUnique).map(JSON.parse));
|
||||
chat.splice(0, messagesToStore.length, ...newChat);
|
||||
|
||||
}
|
||||
@ -467,19 +461,20 @@ jQuery(async () => {
|
||||
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
||||
</div>
|
||||
<div class="inline-drawer-content">
|
||||
<p>This extension rearranges the messages in the current chat to keep more relevant information in the context. Adjust the sliders below based on average amount of messages in your prompt (refer to the chat cut-off line).</p>
|
||||
<span>Memory Injection Strategy</span>
|
||||
<small>This extension rearranges the messages in the current chat to keep more relevant information in the context. Adjust the sliders below based on average amount of messages in your prompt (refer to the chat cut-off line).</small>
|
||||
<span class="wide100p marginTopBot5 displayBlock">Memory Injection Strategy</span>
|
||||
<hr>
|
||||
<select id="chromadb_strategy">
|
||||
<option value="original">Replace non-kept chat items with memories</option>
|
||||
<option value="ross">Add memories after chat with a header tag</option>
|
||||
</select>
|
||||
<label for="chromadb_keep_context">How many original chat messages to keep: (<span id="chromadb_keep_context_value"></span>) messages</label>
|
||||
<label for="chromadb_keep_context"><small>How many original chat messages to keep: (<span id="chromadb_keep_context_value"></span>) messages</small></label>
|
||||
<input id="chromadb_keep_context" type="range" min="${defaultSettings.keep_context_min}" max="${defaultSettings.keep_context_max}" step="${defaultSettings.keep_context_step}" value="${defaultSettings.keep_context}" />
|
||||
<label for="chromadb_n_results">Maximum number of ChromaDB 'memories' to inject: (<span id="chromadb_n_results_value"></span>) messages</label>
|
||||
<label for="chromadb_n_results"><small>Maximum number of ChromaDB 'memories' to inject: (<span id="chromadb_n_results_value"></span>) messages</small></label>
|
||||
<input id="chromadb_n_results" type="range" min="${defaultSettings.n_results_min}" max="${defaultSettings.n_results_max}" step="${defaultSettings.n_results_step}" value="${defaultSettings.n_results}" />
|
||||
<label for="chromadb_split_length">Max length for each 'memory' pulled from the current chat history: (<span id="chromadb_split_length_value"></span>) characters</label>
|
||||
<label for="chromadb_split_length"><small>Max length for each 'memory' pulled from the current chat history: (<span id="chromadb_split_length_value"></span>) characters</small></label>
|
||||
<input id="chromadb_split_length" type="range" min="${defaultSettings.split_length_min}" max="${defaultSettings.split_length_max}" step="${defaultSettings.split_length_step}" value="${defaultSettings.split_length}" />
|
||||
<label for="chromadb_file_split_length">Max length for each 'memory' pulled from imported text files: (<span id="chromadb_file_split_length_value"></span>) characters</label>
|
||||
<label for="chromadb_file_split_length"><small>Max length for each 'memory' pulled from imported text files: (<span id="chromadb_file_split_length_value"></span>) characters</small></label>
|
||||
<input id="chromadb_file_split_length" type="range" min="${defaultSettings.file_split_length_min}" max="${defaultSettings.file_split_length_max}" step="${defaultSettings.file_split_length_step}" value="${defaultSettings.file_split_length}" />
|
||||
<label class="checkbox_label" for="chromadb_freeze" title="Pauses the automatic synchronization of new messages with ChromaDB. Older messages and injections will still be pulled as usual." >
|
||||
<input type="checkbox" id="chromadb_freeze" />
|
||||
@ -509,7 +504,7 @@ jQuery(async () => {
|
||||
<form><input id="chromadb_import_file" type="file" accept="application/json" hidden></form>
|
||||
</div>`;
|
||||
|
||||
$('#extensions_settings').append(settingsHtml);
|
||||
$('#extensions_settings2').append(settingsHtml);
|
||||
$('#chromadb_strategy').on('change', onStrategyChange);
|
||||
$('#chromadb_keep_context').on('input', onKeepContextInput);
|
||||
$('#chromadb_n_results').on('input', onNResultsInput);
|
||||
|
@ -10,5 +10,5 @@
|
||||
"css": "style.css",
|
||||
"author": "maceter636@proton.me",
|
||||
"version": "1.0.0",
|
||||
"homePage": "https://github.com/Cohee1207/SillyTavern"
|
||||
"homePage": "https://github.com/SillyTavern/SillyTavern"
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { getStringHash, debounce } from "../../utils.js";
|
||||
import { getContext, getApiUrl, extension_settings } from "../../extensions.js";
|
||||
import { getContext, getApiUrl, extension_settings, ModuleWorkerWrapper, doExtrasFetch } from "../../extensions.js";
|
||||
import { extension_prompt_types, is_send_press, saveSettingsDebounced } from "../../../script.js";
|
||||
export { MODULE_NAME };
|
||||
|
||||
@ -129,24 +129,6 @@ function getLatestMemoryFromChat(chat) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let isWorkerBusy = false;
|
||||
|
||||
async function moduleWorkerWrapper() {
|
||||
// Don't touch me I'm busy...
|
||||
if (isWorkerBusy) {
|
||||
return;
|
||||
}
|
||||
|
||||
// I'm free. Let's update!
|
||||
try {
|
||||
isWorkerBusy = true;
|
||||
await moduleWorker();
|
||||
}
|
||||
finally {
|
||||
isWorkerBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function moduleWorker() {
|
||||
const context = getContext();
|
||||
const chat = context.chat;
|
||||
@ -250,7 +232,7 @@ async function summarizeChat(context) {
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/summarize';
|
||||
|
||||
const apiResult = await fetch(url, {
|
||||
const apiResult = await doExtrasFetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@ -339,39 +321,39 @@ $(document).ready(function () {
|
||||
<div id="memory_settings">
|
||||
<div class="inline-drawer">
|
||||
<div class="inline-drawer-toggle inline-drawer-header">
|
||||
<b>Chat memory</b>
|
||||
<b>Summarize</b>
|
||||
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
||||
</div>
|
||||
<div class="inline-drawer-content">
|
||||
<label for="memory_contents">Memory contents</label>
|
||||
<label for="memory_contents">Current summary: </label>
|
||||
<textarea id="memory_contents" class="text_pole" rows="8" placeholder="Context will be generated here..."></textarea>
|
||||
<div class="memory_contents_controls">
|
||||
<input id="memory_restore" class="menu_button" type="submit" value="Restore previous state" />
|
||||
<label for="memory_frozen"><input id="memory_frozen" type="checkbox" /> Freeze context</label>
|
||||
<label for="memory_frozen"><input id="memory_frozen" type="checkbox" />Stop summarization updates</label>
|
||||
</div>
|
||||
</div>
|
||||
<!--</div>
|
||||
</div>
|
||||
<div class="inline-drawer">
|
||||
<div class="inline-drawer-toggle inline-drawer-header">
|
||||
<b>Summarization parameters</b>
|
||||
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
||||
</div>
|
||||
<div class="inline-drawer-content">
|
||||
<label for="memory_short_length">Buffer <small>[short-term]</small> length (<span id="memory_short_length_tokens"></span> tokens)</label>
|
||||
<div class="inline-drawer-content">-->
|
||||
<label for="memory_short_length">Chat to Summarize buffer length (<span id="memory_short_length_tokens"></span> tokens)</label>
|
||||
<input id="memory_short_length" type="range" value="${defaultSettings.shortMemoryLength}" min="${defaultSettings.minShortMemory}" max="${defaultSettings.maxShortMemory}" step="${defaultSettings.shortMemoryStep}" />
|
||||
<label for="memory_long_length">Summary <small>[long-term]</small> length (<span id="memory_long_length_tokens"></span> tokens)</label>
|
||||
<label for="memory_long_length">Summary output length (<span id="memory_long_length_tokens"></span> tokens)</label>
|
||||
<input id="memory_long_length" type="range" value="${defaultSettings.longMemoryLength}" min="${defaultSettings.minLongMemory}" max="${defaultSettings.maxLongMemory}" step="${defaultSettings.longMemoryStep}" />
|
||||
<label for="memory_temperature">Temperature (<span id="memory_temperature_value"></span>)</label>
|
||||
<input id="memory_temperature" type="range" value="${defaultSettings.temperature}" min="${defaultSettings.minTemperature}" max="${defaultSettings.maxTemperature}" step="${defaultSettings.temperatureStep}" />
|
||||
<label for="memory_repetition_penalty">Repetition penalty (<span id="memory_repetition_penalty_value"></span>)</label>
|
||||
<input id="memory_repetition_penalty" type="range" value="${defaultSettings.repetitionPenalty}" min="${defaultSettings.minRepetitionPenalty}" max="${defaultSettings.maxRepetitionPenalty}" step="${defaultSettings.repetitionPenaltyStep}" />
|
||||
<label for="memory_length_penalty">Length penalty <small>[higher = longer summaries]</small> (<span id="memory_length_penalty_value"></span>)</label>
|
||||
<label for="memory_length_penalty">Length preference <small>[higher = longer summaries]</small> (<span id="memory_length_penalty_value"></span>)</label>
|
||||
<input id="memory_length_penalty" type="range" value="${defaultSettings.lengthPenalty}" min="${defaultSettings.minLengthPenalty}" max="${defaultSettings.maxLengthPenalty}" step="${defaultSettings.lengthPenaltyStep}" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
$('#extensions_settings').append(settingsHtml);
|
||||
$('#extensions_settings2').append(settingsHtml);
|
||||
$('#memory_restore').on('click', onMemoryRestoreClick);
|
||||
$('#memory_contents').on('input', onMemoryContentInput);
|
||||
$('#memory_long_length').on('input', onMemoryLongInput);
|
||||
@ -384,5 +366,6 @@ $(document).ready(function () {
|
||||
|
||||
addExtensionControls();
|
||||
loadSettings();
|
||||
setInterval(moduleWorkerWrapper, UPDATE_INTERVAL);
|
||||
const wrapper = new ModuleWorkerWrapper(moduleWorker);
|
||||
setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL);
|
||||
});
|
@ -9,5 +9,5 @@
|
||||
"css": "style.css",
|
||||
"author": "Cohee#1207",
|
||||
"version": "1.0.0",
|
||||
"homePage": "https://github.com/Cohee1207/SillyTavern"
|
||||
"homePage": "https://github.com/SillyTavern/SillyTavern"
|
||||
}
|
481
public/scripts/extensions/objective/index.js
Normal file
481
public/scripts/extensions/objective/index.js
Normal file
@ -0,0 +1,481 @@
|
||||
import { chat_metadata } from "../../../script.js";
|
||||
import { getContext, extension_settings, saveMetadataDebounced } from "../../extensions.js";
|
||||
import {
|
||||
substituteParams,
|
||||
eventSource,
|
||||
event_types,
|
||||
generateQuietPrompt,
|
||||
} from "../../../script.js";
|
||||
import { registerSlashCommand } from "../../slash-commands.js";
|
||||
|
||||
const MODULE_NAME = "Objective"
|
||||
|
||||
|
||||
let globalObjective = ""
|
||||
let globalTasks = []
|
||||
let currentChatId = ""
|
||||
let currentTask = null
|
||||
let checkCounter = 0
|
||||
|
||||
|
||||
const objectivePrompts = {
|
||||
"createTask": `Pause your roleplay and generate a list of tasks to complete an objective. Your next response must be formatted as a numbered list of plain text entries. Do not include anything but the numbered list. The list must be prioritized in the order that tasks must be completed.
|
||||
|
||||
The objective that you must make a numbered task list for is: [{{objective}}].
|
||||
The tasks created should take into account the character traits of {{char}}. These tasks may or may not involve {{user}} directly. Be sure to include the objective as the final task.
|
||||
|
||||
Given an example objective of 'Make me a four course dinner', here is an example output:
|
||||
1. Determine what the courses will be
|
||||
2. Find recipes for each course
|
||||
3. Go shopping for supplies with {{user}}
|
||||
4. Cook the food
|
||||
5. Get {{user}} to set the table
|
||||
6. Serve the food
|
||||
7. Enjoy eating the meal with {{user}}
|
||||
`,
|
||||
"checkTaskCompleted": `Pause your roleplay. Determine if this task is completed: [{{task}}].
|
||||
To do this, examine the most recent messages. Your response must only contain either true or false, nothing other words.
|
||||
Example output:
|
||||
true
|
||||
`
|
||||
}
|
||||
|
||||
const extensionPrompt = "Your current task is [{{task}}]. Balance existing roleplay with completing this task."
|
||||
|
||||
|
||||
//###############################//
|
||||
//# Task Management #//
|
||||
//###############################//
|
||||
|
||||
// Accepts optional index. Defaults to adding to end of list.
|
||||
function addTask(description, index = null) {
|
||||
index = index != null ? index: index = globalTasks.length
|
||||
globalTasks.splice(index, 0, new ObjectiveTask(
|
||||
{description: description}
|
||||
))
|
||||
saveState()
|
||||
}
|
||||
|
||||
// Return the task and index or throw an error
|
||||
function getTaskById(taskId){
|
||||
if (taskId == null) {
|
||||
throw `Null task id`
|
||||
}
|
||||
const index = globalTasks.findIndex((task) => task.id === taskId);
|
||||
if (index !== -1) {
|
||||
return { task: globalTasks[index], index: index };
|
||||
} else {
|
||||
throw `Cannot find task with ${taskId}`
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function deleteTask(taskId){
|
||||
const { task, index } = getTaskById(taskId)
|
||||
|
||||
globalTasks.splice(index, 1)
|
||||
setCurrentTask()
|
||||
updateUiTaskList()
|
||||
}
|
||||
|
||||
// Call Quiet Generate to create task list using character context, then convert to tasks. Should not be called much.
|
||||
async function generateTasks() {
|
||||
const prompt = substituteParams(objectivePrompts["createTask"].replace(/{{objective}}/gi, globalObjective));
|
||||
console.log(`Generating tasks for objective with prompt`)
|
||||
toastr.info('Generating tasks for objective', 'Please wait...');
|
||||
const taskResponse = await generateQuietPrompt(prompt)
|
||||
|
||||
// Clear all existing global tasks when generating
|
||||
globalTasks = []
|
||||
const numberedListPattern = /^\d+\./
|
||||
|
||||
// Create tasks from generated task list
|
||||
for (const task of taskResponse.split('\n').map(x => x.trim())) {
|
||||
if (task.match(numberedListPattern) != null) {
|
||||
addTask(task.replace(numberedListPattern,"").trim())
|
||||
}
|
||||
}
|
||||
updateUiTaskList()
|
||||
console.info(`Response for Objective: '${globalObjective}' was \n'${taskResponse}', \nwhich created tasks \n${JSON.stringify(globalTasks.map(v => {return v.toSaveState()}), null, 2)} `)
|
||||
toastr.success(`Generated ${globalTasks.length} tasks`, 'Done!');
|
||||
}
|
||||
|
||||
// Call Quiet Generate to check if a task is completed
|
||||
async function checkTaskCompleted() {
|
||||
// Make sure there are tasks
|
||||
if (jQuery.isEmptyObject(currentTask)) {
|
||||
return
|
||||
}
|
||||
checkCounter = $('#objective-check-frequency').val()
|
||||
|
||||
const prompt = substituteParams(objectivePrompts["checkTaskCompleted"].replace(/{{task}}/gi, currentTask.description));
|
||||
const taskResponse = (await generateQuietPrompt(prompt)).toLowerCase()
|
||||
|
||||
// Check response if task complete
|
||||
if (taskResponse.includes("true")) {
|
||||
console.info(`Character determined task '${JSON.stringify(currentTask.toSaveState())} is completed.`)
|
||||
currentTask.completeTask()
|
||||
} else if (!(taskResponse.includes("false"))) {
|
||||
console.warn(`checkTaskCompleted response did not contain true or false. taskResponse: ${taskResponse}`)
|
||||
} else {
|
||||
console.debug(`Checked task completion. taskResponse: ${taskResponse}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Set a task in extensionPrompt context. Defaults to first incomplete
|
||||
function setCurrentTask(taskId = null) {
|
||||
const context = getContext();
|
||||
currentTask = {};
|
||||
|
||||
// Set current task to either the next incomplete task, or the index
|
||||
if (taskId === null) {
|
||||
currentTask = globalTasks.find(task => !task.completed) || {};
|
||||
} else {
|
||||
const { _, index } = getTaskById(taskId)
|
||||
currentTask = globalTasks[index];
|
||||
}
|
||||
|
||||
// Get the task description and add to extension prompt
|
||||
const description = currentTask.description || null;
|
||||
|
||||
// Now update the extension prompt
|
||||
|
||||
if (description) {
|
||||
const extensionPromptText = extensionPrompt.replace(/{{task}}/gi, description);
|
||||
context.setExtensionPrompt(MODULE_NAME, extensionPromptText, 1, $('#objective-chat-depth').val());
|
||||
console.info(`Current task in context.extensionPrompts.Objective is ${JSON.stringify(context.extensionPrompts.Objective)}`);
|
||||
} else {
|
||||
context.setExtensionPrompt(MODULE_NAME, '');
|
||||
console.info(`No current task`);
|
||||
}
|
||||
|
||||
saveState();
|
||||
}
|
||||
|
||||
let taskIdCounter = 0
|
||||
function getNextTaskId(){
|
||||
// Make sure id does not exist
|
||||
while (globalTasks.find(task => task.id == taskIdCounter) != undefined) {
|
||||
taskIdCounter += 1
|
||||
}
|
||||
const nextId = taskIdCounter
|
||||
console.log(`TaskID assigned: ${nextId}`)
|
||||
taskIdCounter += 1
|
||||
return nextId
|
||||
}
|
||||
class ObjectiveTask {
|
||||
id
|
||||
description
|
||||
completed
|
||||
parent
|
||||
children
|
||||
|
||||
// UI Elements
|
||||
taskHtml
|
||||
descriptionSpan
|
||||
completedCheckbox
|
||||
deleteTaskButton
|
||||
addTaskButton
|
||||
|
||||
constructor ({id=undefined, description, completed=false, parent=null}) {
|
||||
this.description = description
|
||||
this.parent = parent
|
||||
this.children = []
|
||||
this.completed = completed
|
||||
|
||||
// Generate a new ID if none specified
|
||||
if (id==undefined){
|
||||
this.id = getNextTaskId()
|
||||
} else {
|
||||
this.id=id
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Complete the current task, setting next task to next incomplete task
|
||||
completeTask() {
|
||||
this.completed = true
|
||||
console.info(`Task successfully completed: ${JSON.stringify(this.description)}`)
|
||||
setCurrentTask()
|
||||
updateUiTaskList()
|
||||
}
|
||||
|
||||
// Add a single task to the UI and attach event listeners for user edits
|
||||
addUiElement() {
|
||||
const template = `
|
||||
<div id="objective-task-label-${this.id}" class="flex1 checkbox_label">
|
||||
<input id="objective-task-complete-${this.id}" type="checkbox">
|
||||
<span class="text_pole" style="display: block" id="objective-task-description-${this.id}" contenteditable>${this.description}</span>
|
||||
<div id="objective-task-delete-${this.id}" class="objective-task-button fa-solid fa-xmark fa-2x" title="Delete Task"></div>
|
||||
<div id="objective-task-add-${this.id}" class="objective-task-button fa-solid fa-plus fa-2x" title="Add Task"></div>
|
||||
</div><br>
|
||||
`;
|
||||
|
||||
// Add the filled out template
|
||||
$('#objective-tasks').append(template);
|
||||
|
||||
this.completedCheckbox = $(`#objective-task-complete-${this.id}`);
|
||||
this.descriptionSpan = $(`#objective-task-description-${this.id}`);
|
||||
this.addButton = $(`#objective-task-add-${this.id}`);
|
||||
this.deleteButton = $(`#objective-task-delete-${this.id}`);
|
||||
|
||||
// Add event listeners and set properties
|
||||
$(`#objective-task-complete-${this.id}`).prop('checked', this.completed);
|
||||
$(`#objective-task-complete-${this.id}`).on('click', () => (this.onCompleteClick()));
|
||||
$(`#objective-task-description-${this.id}`).on('keyup', () => (this.onDescriptionUpdate()));
|
||||
$(`#objective-task-description-${this.id}`).on('focusout', () => (this.onDescriptionFocusout()));
|
||||
$(`#objective-task-delete-${this.id}`).on('click', () => (this.onDeleteClick()));
|
||||
$(`#objective-task-add-${this.id}`).on('click', () => (this.onAddClick()));
|
||||
}
|
||||
|
||||
onCompleteClick(){
|
||||
this.completed = this.completedCheckbox.prop('checked')
|
||||
setCurrentTask();
|
||||
}
|
||||
|
||||
onDescriptionUpdate(){
|
||||
this.description = this.descriptionSpan.text();
|
||||
}
|
||||
onDescriptionFocusout(){
|
||||
setCurrentTask();
|
||||
}
|
||||
|
||||
onDeleteClick(){
|
||||
deleteTask(this.id);
|
||||
}
|
||||
|
||||
onAddClick(){
|
||||
const {_, index} = getTaskById(this.id)
|
||||
addTask("", index + 1);
|
||||
setCurrentTask();
|
||||
updateUiTaskList();
|
||||
}
|
||||
|
||||
toSaveState() {
|
||||
return {
|
||||
"id":this.id,
|
||||
"description":this.description,
|
||||
"completed":this.completed,
|
||||
"parent": this.parent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//###############################//
|
||||
//# UI AND Settings #//
|
||||
//###############################//
|
||||
|
||||
|
||||
const defaultSettings = {
|
||||
objective: "",
|
||||
tasks: [],
|
||||
chatDepth: 2,
|
||||
checkFrequency: 3,
|
||||
hideTasks: false
|
||||
}
|
||||
|
||||
// Convenient single call. Not much at the moment.
|
||||
function resetState() {
|
||||
loadSettings();
|
||||
}
|
||||
|
||||
//
|
||||
function saveState() {
|
||||
const context = getContext();
|
||||
|
||||
if (currentChatId == "") {
|
||||
currentChatId = context.chatId
|
||||
}
|
||||
|
||||
// Convert globalTasks for saving
|
||||
const tasks = globalTasks.map(task => {return task.toSaveState()})
|
||||
|
||||
chat_metadata['objective'] = {
|
||||
objective: globalObjective,
|
||||
tasks: tasks,
|
||||
checkFrequency: $('#objective-check-frequency').val(),
|
||||
chatDepth: $('#objective-chat-depth').val(),
|
||||
hideTasks: $('#objective-hide-tasks').prop('checked'),
|
||||
}
|
||||
|
||||
saveMetadataDebounced();
|
||||
}
|
||||
|
||||
// Dump core state
|
||||
function debugObjectiveExtension() {
|
||||
console.log(JSON.stringify({
|
||||
"currentTask": currentTask.toSaveState(),
|
||||
"currentChatId": currentChatId,
|
||||
"checkCounter": checkCounter,
|
||||
"globalObjective": globalObjective,
|
||||
"globalTasks": globalTasks.map(v => {return v.toSaveState()}),
|
||||
"extension_settings": chat_metadata['objective'],
|
||||
}, null, 2))
|
||||
}
|
||||
|
||||
window.debugObjectiveExtension = debugObjectiveExtension
|
||||
|
||||
|
||||
// Populate UI task list
|
||||
function updateUiTaskList() {
|
||||
$('#objective-tasks').empty()
|
||||
// Show tasks if there are any
|
||||
if (globalTasks.length > 0){
|
||||
for (const task of globalTasks) {
|
||||
task.addUiElement()
|
||||
}
|
||||
} else {
|
||||
// Show button to add tasks if there are none
|
||||
$('#objective-tasks').append(`
|
||||
<input id="objective-task-add-first" type="button" class="menu_button" value="Add Task">
|
||||
`)
|
||||
$("#objective-task-add-first").on('click', () => {
|
||||
addTask("")
|
||||
setCurrentTask()
|
||||
updateUiTaskList()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Trigger creation of new tasks with given objective.
|
||||
async function onGenerateObjectiveClick() {
|
||||
globalObjective = $('#objective-text').val()
|
||||
await generateTasks()
|
||||
saveState()
|
||||
}
|
||||
|
||||
// Update extension prompts
|
||||
function onChatDepthInput() {
|
||||
saveState()
|
||||
setCurrentTask() // Ensure extension prompt is updated
|
||||
}
|
||||
|
||||
// Update how often we check for task completion
|
||||
function onCheckFrequencyInput() {
|
||||
checkCounter = $("#objective-check-frequency").val()
|
||||
$('#objective-counter').text(checkCounter)
|
||||
saveState()
|
||||
}
|
||||
|
||||
function onHideTasksInput() {
|
||||
$('#objective-tasks').prop('hidden', $('#objective-hide-tasks').prop('checked'))
|
||||
saveState()
|
||||
}
|
||||
|
||||
function loadSettings() {
|
||||
// Load/Init settings for chatId
|
||||
currentChatId = getContext().chatId
|
||||
|
||||
// Bail on home screen
|
||||
if (currentChatId == undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
// Migrate existing settings
|
||||
if (currentChatId in extension_settings.objective) {
|
||||
chat_metadata['objective'] = extension_settings.objective[currentChatId];
|
||||
delete extension_settings.objective[currentChatId];
|
||||
}
|
||||
|
||||
if (!('objective' in chat_metadata)) {
|
||||
Object.assign(chat_metadata, { objective: defaultSettings });
|
||||
}
|
||||
|
||||
// Update globals
|
||||
globalObjective = chat_metadata['objective'].objective
|
||||
globalTasks = chat_metadata['objective'].tasks.map(task => {
|
||||
return new ObjectiveTask({
|
||||
id: task.id,
|
||||
description: task.description,
|
||||
completed: task.completed,
|
||||
parent: task.parent,
|
||||
})
|
||||
});
|
||||
checkCounter = chat_metadata['objective'].checkFrequency
|
||||
|
||||
// Update UI elements
|
||||
$('#objective-counter').text(checkCounter)
|
||||
$("#objective-text").text(globalObjective)
|
||||
updateUiTaskList()
|
||||
$('#objective-chat-depth').val(chat_metadata['objective'].chatDepth)
|
||||
$('#objective-check-frequency').val(chat_metadata['objective'].checkFrequency)
|
||||
$('#objective-hide-tasks').prop('checked', chat_metadata['objective'].hideTasks)
|
||||
onHideTasksInput()
|
||||
setCurrentTask()
|
||||
}
|
||||
|
||||
function addManualTaskCheckUi() {
|
||||
$('#extensionsMenu').prepend(`
|
||||
<div id="objective-task-manual-check-menu-item" class="list-group-item flex-container flexGap5">
|
||||
<div id="objective-task-manual-check" class="extensionsMenuExtensionButton fa-regular fa-square-check"/></div>
|
||||
Manual Task Check
|
||||
</div>`)
|
||||
$('#objective-task-manual-check-menu-item').attr('title', 'Trigger AI check of completed tasks').on('click', checkTaskCompleted)
|
||||
}
|
||||
|
||||
jQuery(() => {
|
||||
const settingsHtml = `
|
||||
<div class="objective-settings">
|
||||
<div class="inline-drawer">
|
||||
<div class="inline-drawer-toggle inline-drawer-header">
|
||||
<b>Objective</b>
|
||||
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
||||
</div>
|
||||
<div class="inline-drawer-content">
|
||||
<label for="objective-text"><small>Enter an objective and generate tasks. The AI will attempt to complete tasks autonomously</small></label>
|
||||
<textarea id="objective-text" type="text" class="text_pole textarea_compact" rows="4"></textarea>
|
||||
<div class="objective_block flex-container">
|
||||
<input id="objective-generate" class="menu_button" type="submit" value="Auto-Generate Tasks" />
|
||||
<label class="checkbox_label"><input id="objective-hide-tasks" type="checkbox"> Hide Tasks</label>
|
||||
</div>
|
||||
|
||||
<div id="objective-tasks"> </div>
|
||||
<div class="objective_block margin-bot-10px">
|
||||
<div class="objective_block objective_block_control flex1 flexFlowColumn">
|
||||
<label for="objective-chat-depth">Position in Chat</label>
|
||||
<input id="objective-chat-depth" class="text_pole widthUnset" type="number" min="0" max="99" />
|
||||
</div>
|
||||
<br>
|
||||
<div class="objective_block objective_block_control flex1">
|
||||
|
||||
<label for="objective-check-frequency">Task Check Frequency</label>
|
||||
<input id="objective-check-frequency" class="text_pole widthUnset" type="number" min="0" max="99" />
|
||||
<small>(0 = disabled)</small>
|
||||
</div>
|
||||
</div>
|
||||
<span> Messages until next AI task completion check <span id="objective-counter">0</span></span>
|
||||
<hr class="sysHR">
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
addManualTaskCheckUi()
|
||||
$('#extensions_settings').append(settingsHtml);
|
||||
$('#objective-generate').on('click', onGenerateObjectiveClick)
|
||||
$('#objective-chat-depth').on('input', onChatDepthInput)
|
||||
$("#objective-check-frequency").on('input', onCheckFrequencyInput)
|
||||
$('#objective-hide-tasks').on('click', onHideTasksInput)
|
||||
loadSettings()
|
||||
|
||||
eventSource.on(event_types.CHAT_CHANGED, () => {
|
||||
resetState()
|
||||
});
|
||||
|
||||
eventSource.on(event_types.MESSAGE_RECEIVED, () => {
|
||||
if (currentChatId == undefined) {
|
||||
return
|
||||
}
|
||||
if ($("#objective-check-frequency").val() > 0) {
|
||||
// Check only at specified interval
|
||||
if (checkCounter <= 0) {
|
||||
checkTaskCompleted();
|
||||
}
|
||||
checkCounter -= 1
|
||||
}
|
||||
setCurrentTask();
|
||||
$('#objective-counter').text(checkCounter)
|
||||
});
|
||||
|
||||
registerSlashCommand('taskcheck', checkTaskCompleted, [], ' – checks if the current task is completed', true, true);
|
||||
});
|
11
public/scripts/extensions/objective/manifest.json
Normal file
11
public/scripts/extensions/objective/manifest.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"display_name": "Objective",
|
||||
"loading_order": 5,
|
||||
"requires": [],
|
||||
"optional": [],
|
||||
"js": "index.js",
|
||||
"css": "style.css",
|
||||
"author": "Ouoertheo",
|
||||
"version": "0.0.1",
|
||||
"homePage": ""
|
||||
}
|
45
public/scripts/extensions/objective/style.css
Normal file
45
public/scripts/extensions/objective/style.css
Normal file
@ -0,0 +1,45 @@
|
||||
#objective-counter {
|
||||
font-weight: 600;
|
||||
color: orange;
|
||||
}
|
||||
|
||||
.objective_block {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 5px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.objective_block_control {
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.objective_block_control small,
|
||||
.objective_block_control label {
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.objective-task-button {
|
||||
margin: 0;
|
||||
outline: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: 0.3s;
|
||||
opacity: 0.7;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
}
|
||||
|
||||
.objective-task-button:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[id^=objective-task-delete-] {
|
||||
color: #da3f3f;
|
||||
}
|
||||
|
||||
#objective-tasks span {
|
||||
margin: unset;
|
||||
margin-bottom: 5px !important;
|
||||
}
|
174
public/scripts/extensions/quick-reply/index.js
Normal file
174
public/scripts/extensions/quick-reply/index.js
Normal file
@ -0,0 +1,174 @@
|
||||
import { saveSettingsDebounced } from "../../../script.js";
|
||||
import { getContext, extension_settings } from "../../extensions.js";
|
||||
import { initScrollHeight, resetScrollHeight } from "../../utils.js";
|
||||
|
||||
export { MODULE_NAME };
|
||||
|
||||
const MODULE_NAME = 'quick-reply';
|
||||
const UPDATE_INTERVAL = 1000;
|
||||
|
||||
const defaultSettings = {
|
||||
quickReply1Mes: '',
|
||||
quickReply1Label: '',
|
||||
quickReply2Mes: '',
|
||||
quickReply2Label: '',
|
||||
quickReply3Mes: '',
|
||||
quickReply3Label: '',
|
||||
quickReply4Mes: '',
|
||||
quickReply4Label: '',
|
||||
quickReply5Mes: '',
|
||||
quickReply5Label: '',
|
||||
quickReplyEnabled: false,
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
if (Object.keys(extension_settings.quickReply).length === 0) {
|
||||
Object.assign(extension_settings.quickReply, defaultSettings);
|
||||
}
|
||||
|
||||
$('#quickReplyEnabled').prop('checked', extension_settings.quickReply.quickReplyEnabled);
|
||||
|
||||
$('#quickReply1Mes').val(extension_settings.quickReply.quickReply1Mes).trigger('input');
|
||||
$('#quickReply1Label').val(extension_settings.quickReply.quickReply1Label).trigger('input');
|
||||
|
||||
$('#quickReply2Mes').val(extension_settings.quickReply.quickReply2Mes).trigger('input');
|
||||
$('#quickReply2Label').val(extension_settings.quickReply.quickReply2Label).trigger('input');
|
||||
|
||||
$('#quickReply3Mes').val(extension_settings.quickReply.quickReply3Mes).trigger('input');
|
||||
$('#quickReply3Label').val(extension_settings.quickReply.quickReply3Label).trigger('input');
|
||||
|
||||
$('#quickReply4Mes').val(extension_settings.quickReply.quickReply4Mes).trigger('input');
|
||||
$('#quickReply4Label').val(extension_settings.quickReply.quickReply4Label).trigger('input');
|
||||
|
||||
$('#quickReply5Mes').val(extension_settings.quickReply.quickReply5Mes).trigger('input');
|
||||
$('#quickReply5Label').val(extension_settings.quickReply.quickReply5Label).trigger('input');
|
||||
}
|
||||
|
||||
function onQuickReplyInput(id) {
|
||||
extension_settings.quickReply[`quickReply${id}Mes`] = $(`#quickReply${id}Mes`).val();
|
||||
$(`#quickReply${id}`).attr('title', ($(`#quickReply${id}Mes`).val()));
|
||||
resetScrollHeight($(`#quickReply${id}Mes`));
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onQuickReplyLabelInput(id) {
|
||||
extension_settings.quickReply[`quickReply${id}Label`] = $(`#quickReply${id}Label`).val();
|
||||
$(`#quickReply${id}`).text($(`#quickReply${id}Label`).val());
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
async function onQuickReplyEnabledInput() {
|
||||
let isEnabled = $(this).prop('checked')
|
||||
extension_settings.quickReply.quickReplyEnabled = !!isEnabled;
|
||||
if (isEnabled === true) {
|
||||
$("#quickReplyBar").show();
|
||||
} else { $("#quickReplyBar").hide(); }
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
async function sendQuickReply(id) {
|
||||
var prompt = extension_settings.quickReply[`${id}Mes`];
|
||||
$("#send_textarea").val(prompt);
|
||||
$("#send_but").trigger('click');
|
||||
}
|
||||
|
||||
function addQuickReplyBar(numButtons) {
|
||||
var numButtons = 5;
|
||||
const quickReplyBarStartHtml = `
|
||||
<div id="quickReplyBar" class="flex-container flexGap5">
|
||||
<div id="quickReplies">
|
||||
`;
|
||||
let quickReplyButtonHtml = '';
|
||||
for (let i = 0; i < numButtons; i++) {
|
||||
let quickReplyMes = extension_settings.quickReply[`quickReply${i + 1}Mes`];
|
||||
let quickReplyLabel = extension_settings.quickReply[`quickReply${i + 1}Label`];
|
||||
//console.log(quickReplyMes);
|
||||
quickReplyButtonHtml += `<div title="${quickReplyMes}" class="quickReplyButton" id="quickReply${i + 1}">${quickReplyLabel}</div>`;
|
||||
}
|
||||
const quickReplyEndHtml = `</div></div>`
|
||||
const quickReplyBarFullHtml = [quickReplyBarStartHtml, quickReplyButtonHtml, quickReplyEndHtml].join('');
|
||||
|
||||
$('#send_form').prepend(quickReplyBarFullHtml);
|
||||
|
||||
$('.quickReplyButton').on('click', function () {
|
||||
console.log('got quick reply click');
|
||||
let quickReplyButtonID = $(this).attr('id');
|
||||
sendQuickReply(quickReplyButtonID);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async function moduleWorker() {
|
||||
if (extension_settings.quickReply.quickReplyEnabled === true) {
|
||||
$('#quickReplyBar').toggle(getContext().onlineStatus !== 'no_connection');
|
||||
}
|
||||
}
|
||||
|
||||
jQuery(async () => {
|
||||
|
||||
moduleWorker();
|
||||
setInterval(moduleWorker, UPDATE_INTERVAL);
|
||||
const settingsHtml = `
|
||||
<div class="quickReplySettings">
|
||||
<div class="inline-drawer">
|
||||
<div class="inline-drawer-toggle inline-drawer-header">
|
||||
<b>Quick Reply</b>
|
||||
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
||||
</div>
|
||||
<div class="inline-drawer-content">
|
||||
<label class="checkbox_label">
|
||||
<input id="quickReplyEnabled" type="checkbox" />
|
||||
Enable Quick Replies
|
||||
</label>
|
||||
<small><i>Customize your Quick Replies:</i></small><br>
|
||||
<div class="flex-container alignitemsflexstart">
|
||||
<input class="text_pole wide30p" id="quickReply1Label" placeholder="(Add a button label)">
|
||||
<textarea id="quickReply1Mes" placeholder="(custom message here)" class="text_pole textarea_compact widthUnset flex1" rows="2"></textarea>
|
||||
</div>
|
||||
<div class="flex-container alignitemsflexstart">
|
||||
<input class="text_pole wide30p" id="quickReply2Label" placeholder="(Add a button label)">
|
||||
<textarea id="quickReply2Mes" placeholder="(custom message here)" class="text_pole textarea_compact widthUnset flex1" rows="2"></textarea>
|
||||
</div>
|
||||
<div class="flex-container alignitemsflexstart">
|
||||
<input class="text_pole wide30p" id="quickReply3Label" placeholder="(Add a button label)">
|
||||
<textarea id="quickReply3Mes" placeholder="(custom message here)" class="text_pole textarea_compact widthUnset flex1" rows="2"></textarea>
|
||||
</div>
|
||||
<div class="flex-container alignitemsflexstart">
|
||||
<input class="text_pole wide30p" id="quickReply4Label" placeholder="(Add a button label)">
|
||||
<textarea id="quickReply4Mes" placeholder="(custom message here)" class="text_pole textarea_compact widthUnset flex1" rows="2"></textarea>
|
||||
</div>
|
||||
<div class="flex-container alignitemsflexstart">
|
||||
<input class="text_pole wide30p" id="quickReply5Label" placeholder="(Add a button label)">
|
||||
<textarea id="quickReply5Mes" placeholder="(custom message here)" class="text_pole textarea_compact widthUnset flex1" rows="2"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
$('#extensions_settings2').append(settingsHtml);
|
||||
|
||||
$('#quickReply1Mes').on('input', function () { onQuickReplyInput(1); });
|
||||
$('#quickReply2Mes').on('input', function () { onQuickReplyInput(2); });
|
||||
$('#quickReply3Mes').on('input', function () { onQuickReplyInput(3); });
|
||||
$('#quickReply4Mes').on('input', function () { onQuickReplyInput(4); });
|
||||
$('#quickReply5Mes').on('input', function () { onQuickReplyInput(5); });
|
||||
|
||||
$('#quickReply1Label').on('input', function () { onQuickReplyLabelInput(1); });
|
||||
$('#quickReply2Label').on('input', function () { onQuickReplyLabelInput(2); });
|
||||
$('#quickReply3Label').on('input', function () { onQuickReplyLabelInput(3); });
|
||||
$('#quickReply4Label').on('input', function () { onQuickReplyLabelInput(4); });
|
||||
$('#quickReply5Label').on('input', function () { onQuickReplyLabelInput(5); });
|
||||
|
||||
$('#quickReplyEnabled').on('input', onQuickReplyEnabledInput);
|
||||
|
||||
$('.quickReplySettings .inline-drawer-toggle').on('click', function () {
|
||||
initScrollHeight($("#quickReply1Mes"));
|
||||
initScrollHeight($("#quickReply2Mes"));
|
||||
initScrollHeight($("#quickReply3Mes"));
|
||||
initScrollHeight($("#quickReply4Mes"));
|
||||
initScrollHeight($("#quickReply5Mes"));
|
||||
})
|
||||
|
||||
await loadSettings();
|
||||
addQuickReplyBar();
|
||||
});
|
||||
|
11
public/scripts/extensions/quick-reply/manifest.json
Normal file
11
public/scripts/extensions/quick-reply/manifest.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"display_name": "Quick Replies",
|
||||
"loading_order": 12,
|
||||
"requires": [],
|
||||
"optional": [],
|
||||
"js": "index.js",
|
||||
"css": "style.css",
|
||||
"author": "RossAscends#1779",
|
||||
"version": "1.0.0",
|
||||
"homePage": "https://github.com/SillyTavern/SillyTavern"
|
||||
}
|
44
public/scripts/extensions/quick-reply/style.css
Normal file
44
public/scripts/extensions/quick-reply/style.css
Normal file
@ -0,0 +1,44 @@
|
||||
#quickReplyBar {
|
||||
outline: none;
|
||||
padding: 5px 0;
|
||||
border-bottom: 1px solid var(--black30a);
|
||||
margin: 0;
|
||||
transition: 0.3s;
|
||||
opacity: 0.7;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#quickReplies {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: nowrap;
|
||||
gap: 5px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#quickReplies div {
|
||||
color: var(--SmartThemeBodyColor);
|
||||
background-color: var(--black50a);
|
||||
border: 1px solid var(--white30a);
|
||||
border-radius: 10px;
|
||||
padding: 3px 5px;
|
||||
width: min-content;
|
||||
cursor: pointer;
|
||||
transition: 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#quickReplies div:hover {
|
||||
opacity: 1;
|
||||
filter: brightness(1.2);
|
||||
cursor: pointer;
|
||||
}
|
110
public/scripts/extensions/speech-recognition/index.js
Normal file
110
public/scripts/extensions/speech-recognition/index.js
Normal file
@ -0,0 +1,110 @@
|
||||
// Borrowed from Agnai (AGPLv3)
|
||||
// https://github.com/agnaistic/agnai/blob/dev/web/pages/Chat/components/SpeechRecognitionRecorder.tsx
|
||||
function capitalizeInterim(interimTranscript) {
|
||||
let capitalizeIndex = -1;
|
||||
if (interimTranscript.length > 2 && interimTranscript[0] === ' ') capitalizeIndex = 1;
|
||||
else if (interimTranscript.length > 1) capitalizeIndex = 0;
|
||||
if (capitalizeIndex > -1) {
|
||||
const spacing = capitalizeIndex > 0 ? ' '.repeat(capitalizeIndex - 1) : '';
|
||||
const capitalized = interimTranscript[capitalizeIndex].toLocaleUpperCase();
|
||||
const rest = interimTranscript.substring(capitalizeIndex + 1);
|
||||
interimTranscript = spacing + capitalized + rest;
|
||||
}
|
||||
return interimTranscript;
|
||||
}
|
||||
|
||||
function composeValues(previous, interim) {
|
||||
let spacing = '';
|
||||
if (previous.endsWith('.')) spacing = ' ';
|
||||
return previous + spacing + interim;
|
||||
}
|
||||
|
||||
(function ($) {
|
||||
$.fn.speechRecognitionPlugin = function (options) {
|
||||
const settings = $.extend({
|
||||
grammar: '' // Custom grammar
|
||||
}, options);
|
||||
|
||||
const speechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
const speechRecognitionList = window.SpeechGrammarList || window.webkitSpeechGrammarList;
|
||||
|
||||
if (!speechRecognition) {
|
||||
console.warn('Speech recognition is not supported in this browser.');
|
||||
return;
|
||||
}
|
||||
|
||||
const recognition = new speechRecognition();
|
||||
|
||||
if (settings.grammar && speechRecognitionList) {
|
||||
speechRecognitionList.addFromString(settings.grammar, 1);
|
||||
recognition.grammars = speechRecognitionList;
|
||||
}
|
||||
|
||||
recognition.continuous = true;
|
||||
recognition.interimResults = true;
|
||||
// TODO: This should be configurable.
|
||||
recognition.lang = 'en-US'; // Set the language to English (US).
|
||||
|
||||
const $textarea = this;
|
||||
const $button = $('<div class="fa-solid fa-microphone speech-toggle" title="Click to speak"></div>');
|
||||
$('#send_but_sheld').prepend($button);
|
||||
|
||||
let listening = false;
|
||||
$button.on('click', function () {
|
||||
if (listening) {
|
||||
recognition.stop();
|
||||
} else {
|
||||
recognition.start();
|
||||
}
|
||||
listening = !listening;
|
||||
});
|
||||
|
||||
let initialText = '';
|
||||
|
||||
recognition.onresult = function (speechEvent) {
|
||||
let finalTranscript = '';
|
||||
let interimTranscript = ''
|
||||
|
||||
for (let i = speechEvent.resultIndex; i < speechEvent.results.length; ++i) {
|
||||
const transcript = speechEvent.results[i][0].transcript;
|
||||
|
||||
if (speechEvent.results[i].isFinal) {
|
||||
let interim = capitalizeInterim(transcript);
|
||||
if (interim != '') {
|
||||
let final = finalTranscript;
|
||||
final = composeValues(final, interim) + '.';
|
||||
finalTranscript = final;
|
||||
recognition.abort();
|
||||
listening = false;
|
||||
}
|
||||
interimTranscript = ' ';
|
||||
} else {
|
||||
interimTranscript += transcript;
|
||||
}
|
||||
}
|
||||
|
||||
interimTranscript = capitalizeInterim(interimTranscript);
|
||||
|
||||
$textarea.val(initialText + finalTranscript + interimTranscript);
|
||||
};
|
||||
|
||||
recognition.onerror = function (event) {
|
||||
console.error('Error occurred in recognition:', event.error);
|
||||
};
|
||||
|
||||
recognition.onend = function () {
|
||||
listening = false;
|
||||
$button.toggleClass('fa-microphone fa-microphone-slash');
|
||||
};
|
||||
|
||||
recognition.onstart = function () {
|
||||
initialText = $textarea.val();
|
||||
$button.toggleClass('fa-microphone fa-microphone-slash');
|
||||
};
|
||||
};
|
||||
}(jQuery));
|
||||
|
||||
jQuery(() => {
|
||||
const $textarea = $('#send_textarea');
|
||||
$textarea.speechRecognitionPlugin();
|
||||
});
|
11
public/scripts/extensions/speech-recognition/manifest.json
Normal file
11
public/scripts/extensions/speech-recognition/manifest.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"display_name": "Speech Recognition",
|
||||
"loading_order": 13,
|
||||
"requires": [],
|
||||
"optional": [],
|
||||
"js": "index.js",
|
||||
"css": "style.css",
|
||||
"author": "Cohee#1207",
|
||||
"version": "1.0.0",
|
||||
"homePage": "https://github.com/SillyTavern/SillyTavern"
|
||||
}
|
3
public/scripts/extensions/speech-recognition/style.css
Normal file
3
public/scripts/extensions/speech-recognition/style.css
Normal file
@ -0,0 +1,3 @@
|
||||
.speech-toggle {
|
||||
display: flex;
|
||||
}
|
@ -10,7 +10,7 @@ import {
|
||||
eventSource,
|
||||
appendImageToMessage
|
||||
} from "../../../script.js";
|
||||
import { getApiUrl, getContext, extension_settings, defaultRequestArgs, modules } from "../../extensions.js";
|
||||
import { getApiUrl, getContext, extension_settings, doExtrasFetch, modules } from "../../extensions.js";
|
||||
import { stringFormat, initScrollHeight, resetScrollHeight } from "../../utils.js";
|
||||
export { MODULE_NAME };
|
||||
|
||||
@ -234,7 +234,7 @@ async function onModelChange() {
|
||||
async function updateExtrasRemoteModel() {
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/image/model';
|
||||
const getCurrentModelResult = await fetch(url, {
|
||||
const getCurrentModelResult = await doExtrasFetch(url, {
|
||||
method: 'POST',
|
||||
headers: postHeaders,
|
||||
body: JSON.stringify({ model: extension_settings.sd.model }),
|
||||
@ -285,7 +285,7 @@ async function loadExtrasSamplers() {
|
||||
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/image/samplers';
|
||||
const result = await fetch(url, defaultRequestArgs);
|
||||
const result = await doExtrasFetch(url);
|
||||
|
||||
if (result.ok) {
|
||||
const data = await result.json();
|
||||
@ -338,7 +338,7 @@ async function loadExtrasModels() {
|
||||
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/image/model';
|
||||
const getCurrentModelResult = await fetch(url, defaultRequestArgs);
|
||||
const getCurrentModelResult = await doExtrasFetch(url);
|
||||
|
||||
if (getCurrentModelResult.ok) {
|
||||
const data = await getCurrentModelResult.json();
|
||||
@ -346,7 +346,7 @@ async function loadExtrasModels() {
|
||||
}
|
||||
|
||||
url.pathname = '/api/image/models';
|
||||
const getModelsResult = await fetch(url, defaultRequestArgs);
|
||||
const getModelsResult = await doExtrasFetch(url);
|
||||
|
||||
if (getModelsResult.ok) {
|
||||
const data = await getModelsResult.json();
|
||||
@ -459,7 +459,7 @@ async function getPrompt(generationType, message, trigger, quiet_prompt) {
|
||||
prompt = message || getRawLastMessage();
|
||||
break;
|
||||
case generationMode.FREE:
|
||||
prompt = processReply(trigger);
|
||||
prompt = trigger.trim();
|
||||
break;
|
||||
default:
|
||||
prompt = await generatePrompt(quiet_prompt);
|
||||
@ -493,7 +493,7 @@ async function generateExtrasImage(prompt, callback) {
|
||||
console.log(extension_settings.sd);
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/image';
|
||||
const result = await fetch(url, {
|
||||
const result = await doExtrasFetch(url, {
|
||||
method: 'POST',
|
||||
headers: postHeaders,
|
||||
body: JSON.stringify({
|
||||
@ -583,8 +583,9 @@ function addSDGenButtons() {
|
||||
`
|
||||
const dropdownHtml = `
|
||||
<div id="sd_dropdown">
|
||||
<span>Send me a picture of:</span>
|
||||
|
||||
<ul class="list-group">
|
||||
<span>Send me a picture of:</span>
|
||||
<li class="list-group-item" id="sd_you" data-value="you">Yourself</li>
|
||||
<li class="list-group-item" id="sd_face" data-value="face">Your Face</li>
|
||||
<li class="list-group-item" id="sd_me" data-value="me">Me</li>
|
||||
@ -608,7 +609,7 @@ function addSDGenButtons() {
|
||||
messageButton.hide();
|
||||
|
||||
let popper = Popper.createPopper(button.get(0), dropdown.get(0), {
|
||||
placement: 'bottom',
|
||||
placement: 'top',
|
||||
});
|
||||
|
||||
$(document).on('click', '.sd_message_gen', sdMessageButton);
|
||||
@ -619,10 +620,10 @@ function addSDGenButtons() {
|
||||
if (target.is(button) && !dropdown.is(":visible") && $("#send_but").css('display') === 'flex') {
|
||||
e.preventDefault();
|
||||
|
||||
dropdown.show(200);
|
||||
dropdown.fadeIn(250);
|
||||
popper.update();
|
||||
} else {
|
||||
dropdown.hide(200);
|
||||
dropdown.fadeOut(250);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -633,11 +634,11 @@ function isConnectedToExtras() {
|
||||
|
||||
async function moduleWorker() {
|
||||
if (isConnectedToExtras() || extension_settings.sd.horde) {
|
||||
$('#sd_gen').show(200);
|
||||
$('#sd_gen').show();
|
||||
$('.sd_message_gen').show();
|
||||
}
|
||||
else {
|
||||
$('#sd_gen').hide(200);
|
||||
$('#sd_gen').hide();
|
||||
$('.sd_message_gen').hide();
|
||||
}
|
||||
}
|
||||
|
@ -9,5 +9,5 @@
|
||||
"css": "style.css",
|
||||
"author": "Cohee#1207",
|
||||
"version": "1.0.0",
|
||||
"homePage": "https://github.com/Cohee1207/SillyTavern"
|
||||
"homePage": "https://github.com/SillyTavern/SillyTavern"
|
||||
}
|
@ -7,5 +7,5 @@
|
||||
"css": "style.css",
|
||||
"author": "Cohee#1207",
|
||||
"version": "1.0.0",
|
||||
"homePage": "https://github.com/Cohee1207/SillyTavern"
|
||||
"homePage": "https://github.com/SillyTavern/SillyTavern"
|
||||
}
|
||||
|
@ -350,7 +350,7 @@ jQuery(() => {
|
||||
Translate Chat
|
||||
</div>`;
|
||||
$('#extensionsMenu').append(buttonHtml);
|
||||
$('#extensions_settings').append(html);
|
||||
$('#extensions_settings2').append(html);
|
||||
$('#translate_chat').on('click', onTranslateChatClick);
|
||||
$('#translation_clear').on('click', onTranslationsClearClick);
|
||||
|
||||
|
@ -7,5 +7,5 @@
|
||||
"css": "style.css",
|
||||
"author": "Cohee#1207",
|
||||
"version": "1.0.0",
|
||||
"homePage": "https://github.com/Cohee1207/SillyTavern"
|
||||
"homePage": "https://github.com/SillyTavern/SillyTavern"
|
||||
}
|
||||
|
151
public/scripts/extensions/tts/edge.js
Normal file
151
public/scripts/extensions/tts/edge.js
Normal file
@ -0,0 +1,151 @@
|
||||
import { getRequestHeaders } from "../../../script.js"
|
||||
import { getApiUrl } from "../../extensions.js"
|
||||
import { doExtrasFetch, modules } from "../../extensions.js"
|
||||
import { getPreviewString } from "./index.js"
|
||||
|
||||
export { EdgeTtsProvider }
|
||||
|
||||
class EdgeTtsProvider {
|
||||
//########//
|
||||
// Config //
|
||||
//########//
|
||||
|
||||
settings
|
||||
voices = []
|
||||
separator = ' . '
|
||||
audioElement = document.createElement('audio')
|
||||
|
||||
defaultSettings = {
|
||||
voiceMap: {},
|
||||
rate: 0,
|
||||
}
|
||||
|
||||
get settingsHtml() {
|
||||
let html = `Microsoft Edge TTS Provider<br>
|
||||
<label for="edge_tts_rate">Rate: <span id="edge_tts_rate_output"></span></label>
|
||||
<input id="edge_tts_rate" type="range" value="${this.defaultSettings.rate}" min="-100" max="100" step="1" />`
|
||||
return html
|
||||
}
|
||||
|
||||
onSettingsChange() {
|
||||
this.settings.rate = Number($('#edge_tts_rate').val());
|
||||
$('#edge_tts_rate_output').text(this.settings.rate);
|
||||
}
|
||||
|
||||
loadSettings(settings) {
|
||||
// Pupulate Provider UI given input settings
|
||||
if (Object.keys(settings).length == 0) {
|
||||
console.info("Using default TTS Provider settings")
|
||||
}
|
||||
|
||||
// Only accept keys defined in defaultSettings
|
||||
this.settings = this.defaultSettings
|
||||
|
||||
for (const key in settings) {
|
||||
if (key in this.settings) {
|
||||
this.settings[key] = settings[key]
|
||||
} else {
|
||||
throw `Invalid setting passed to TTS Provider: ${key}`
|
||||
}
|
||||
}
|
||||
|
||||
$('#edge_tts_rate').val(this.settings.rate || 0);
|
||||
$('#edge_tts_rate_output').text(this.settings.rate || 0);
|
||||
|
||||
console.info("Settings loaded")
|
||||
}
|
||||
|
||||
|
||||
async onApplyClick() {
|
||||
return
|
||||
}
|
||||
|
||||
//#################//
|
||||
// TTS Interfaces //
|
||||
//#################//
|
||||
|
||||
async getVoice(voiceName) {
|
||||
if (this.voices.length == 0) {
|
||||
this.voices = await this.fetchTtsVoiceIds()
|
||||
}
|
||||
const match = this.voices.filter(
|
||||
voice => voice.name == voiceName
|
||||
)[0]
|
||||
if (!match) {
|
||||
throw `TTS Voice name ${voiceName} not found`
|
||||
}
|
||||
return match
|
||||
}
|
||||
|
||||
async generateTts(text, voiceId) {
|
||||
const response = await this.fetchTtsGeneration(text, voiceId)
|
||||
return response
|
||||
}
|
||||
|
||||
//###########//
|
||||
// API CALLS //
|
||||
//###########//
|
||||
async fetchTtsVoiceIds() {
|
||||
throwIfModuleMissing()
|
||||
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = `/api/edge-tts/list`
|
||||
const response = await doExtrasFetch(url)
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${await response.text()}`)
|
||||
}
|
||||
let responseJson = await response.json()
|
||||
responseJson = responseJson
|
||||
.sort((a, b) => a.Locale.localeCompare(b.Locale) || a.ShortName.localeCompare(b.ShortName))
|
||||
.map(x => ({ name: x.ShortName, voice_id: x.ShortName, preview_url: false, lang: x.Locale }));
|
||||
return responseJson
|
||||
}
|
||||
|
||||
|
||||
async previewTtsVoice(id) {
|
||||
this.audioElement.pause();
|
||||
this.audioElement.currentTime = 0;
|
||||
const voice = await this.getVoice(id);
|
||||
const text = getPreviewString(voice.lang);
|
||||
const response = await this.fetchTtsGeneration(text, id)
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${await response.text()}`)
|
||||
}
|
||||
|
||||
const audio = await response.blob();
|
||||
const url = URL.createObjectURL(audio);
|
||||
this.audioElement.src = url;
|
||||
this.audioElement.play();
|
||||
}
|
||||
|
||||
async fetchTtsGeneration(inputText, voiceId) {
|
||||
throwIfModuleMissing()
|
||||
|
||||
console.info(`Generating new TTS for voice_id ${voiceId}`)
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = `/api/edge-tts/generate`;
|
||||
const response = await doExtrasFetch(url,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({
|
||||
"text": inputText,
|
||||
"voice": voiceId,
|
||||
"rate": Number(this.settings.rate),
|
||||
})
|
||||
}
|
||||
)
|
||||
if (!response.ok) {
|
||||
toastr.error(response.statusText, 'TTS Generation Failed');
|
||||
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
||||
}
|
||||
return response
|
||||
}
|
||||
}
|
||||
function throwIfModuleMissing() {
|
||||
if (!modules.includes('edge-tts')) {
|
||||
toastr.error(`Edge TTS module not loaded. Add edge-tts to enable-modules and restart the Extras API.`)
|
||||
throw new Error(`Edge TTS module not loaded.`)
|
||||
}
|
||||
}
|
||||
|
@ -149,7 +149,7 @@ class ElevenLabsTtsProvider {
|
||||
headers: headers
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
|
||||
throw new Error(`HTTP ${response.status}: ${await response.text()}`)
|
||||
}
|
||||
const responseJson = await response.json()
|
||||
return responseJson.voices
|
||||
@ -166,7 +166,7 @@ class ElevenLabsTtsProvider {
|
||||
}
|
||||
)
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
|
||||
throw new Error(`HTTP ${response.status}: ${await response.text()}`)
|
||||
}
|
||||
return response.json()
|
||||
}
|
||||
@ -193,7 +193,8 @@ class ElevenLabsTtsProvider {
|
||||
}
|
||||
)
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
|
||||
toastr.error(response.statusText, 'TTS Generation Failed');
|
||||
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
||||
}
|
||||
return response
|
||||
}
|
||||
@ -209,7 +210,7 @@ class ElevenLabsTtsProvider {
|
||||
}
|
||||
)
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
|
||||
throw new Error(`HTTP ${response.status}: ${await response.text()}`)
|
||||
}
|
||||
return response
|
||||
}
|
||||
@ -222,7 +223,7 @@ class ElevenLabsTtsProvider {
|
||||
headers: headers
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
|
||||
throw new Error(`HTTP ${response.status}: ${await response.text()}`)
|
||||
}
|
||||
const responseJson = await response.json()
|
||||
return responseJson.history
|
||||
|
@ -1,9 +1,13 @@
|
||||
import { callPopup, cancelTtsPlay, eventSource, event_types, isMultigenEnabled, is_send_press, saveSettingsDebounced } from '../../../script.js'
|
||||
import { extension_settings, getContext } from '../../extensions.js'
|
||||
import { getStringHash } from '../../utils.js'
|
||||
import { ModuleWorkerWrapper, extension_settings, getContext } from '../../extensions.js'
|
||||
import { escapeRegex, getStringHash } from '../../utils.js'
|
||||
import { EdgeTtsProvider } from './edge.js'
|
||||
import { ElevenLabsTtsProvider } from './elevenlabs.js'
|
||||
import { SileroTtsProvider } from './silerotts.js'
|
||||
import { SystemTtsProvider } from './system.js'
|
||||
import { NovelTtsProvider } from './novel.js'
|
||||
import { isMobile } from '../../RossAscends-mods.js'
|
||||
import { power_user } from '../../power-user.js'
|
||||
|
||||
const UPDATE_INTERVAL = 1000
|
||||
|
||||
@ -15,16 +19,59 @@ let lastGroupId = null
|
||||
let lastChatId = null
|
||||
let lastMessageHash = null
|
||||
|
||||
export function getPreviewString(lang) {
|
||||
const previewStrings = {
|
||||
'en-US': 'The quick brown fox jumps over the lazy dog',
|
||||
'en-GB': 'Sphinx of black quartz, judge my vow',
|
||||
'fr-FR': 'Portez ce vieux whisky au juge blond qui fume',
|
||||
'de-DE': 'Victor jagt zwölf Boxkämpfer quer über den großen Sylter Deich',
|
||||
'it-IT': "Pranzo d'acqua fa volti sghembi",
|
||||
'es-ES': 'Quiere la boca exhausta vid, kiwi, piña y fugaz jamón',
|
||||
'es-MX': 'Fabio me exige, sin tapujos, que añada cerveza al whisky',
|
||||
'ru-RU': 'В чащах юга жил бы цитрус? Да, но фальшивый экземпляр!',
|
||||
'pt-BR': 'Vejo xá gritando que fez show sem playback.',
|
||||
'pt-PR': 'Todo pajé vulgar faz boquinha sexy com kiwi.',
|
||||
'uk-UA': "Фабрикуймо гідність, лящім їжею, ґав хапаймо, з'єднавці чаш!",
|
||||
'pl-PL': 'Pchnąć w tę łódź jeża lub ośm skrzyń fig',
|
||||
'cs-CZ': 'Příliš žluťoučký kůň úpěl ďábelské ódy',
|
||||
'sk-SK': 'Vyhŕňme si rukávy a vyprážajme čínske ryžové cestoviny',
|
||||
'hu-HU': 'Árvíztűrő tükörfúrógép',
|
||||
'tr-TR': 'Pijamalı hasta yağız şoföre çabucak güvendi',
|
||||
'nl-NL': 'De waard heeft een kalfje en een pinkje opgegeten',
|
||||
'sv-SE': 'Yxskaftbud, ge vårbygd, zinkqvarn',
|
||||
'da-DK': 'Quizdeltagerne spiste jordbær med fløde, mens cirkusklovnen Walther spillede på xylofon',
|
||||
'ja-JP': 'いろはにほへと ちりぬるを わかよたれそ つねならむ うゐのおくやま けふこえて あさきゆめみし ゑひもせす',
|
||||
'ko-KR': '가나다라마바사아자차카타파하',
|
||||
'zh-CN': '我能吞下玻璃而不伤身体',
|
||||
'ro-RO': 'Muzicologă în bej vând whisky și tequila, preț fix',
|
||||
'bg-BG': 'Щъркелите се разпръснаха по цялото небе',
|
||||
'el-GR': 'Ταχίστη αλώπηξ βαφής ψημένη γη, δρασκελίζει υπέρ νωθρού κυνός',
|
||||
'fi-FI': 'Voi veljet, miksi juuri teille myin nämä vehkeet?',
|
||||
'he-IL': 'הקצינים צעקו: "כל הכבוד לצבא הצבאות!"',
|
||||
'id-ID': 'Jangkrik itu memang enak, apalagi kalau digoreng',
|
||||
'ms-MY': 'Muzik penyanyi wanita itu menggambarkan kehidupan yang penuh dengan duka nestapa',
|
||||
'th-TH': 'เป็นไงบ้างครับ ผมชอบกินข้าวผัดกระเพราหมูกรอบ',
|
||||
'vi-VN': 'Cô bé quàng khăn đỏ đang ngồi trên bãi cỏ xanh',
|
||||
'ar-SA': 'أَبْجَدِيَّة عَرَبِيَّة',
|
||||
'hi-IN': 'श्वेता ने श्वेता के श्वेते हाथों में श्वेता का श्वेता चावल पकड़ा',
|
||||
}
|
||||
const fallbackPreview = 'Neque porro quisquam est qui dolorem ipsum quia dolor sit amet'
|
||||
|
||||
return previewStrings[lang] ?? fallbackPreview;
|
||||
}
|
||||
|
||||
let ttsProviders = {
|
||||
ElevenLabs: ElevenLabsTtsProvider,
|
||||
Silero: SileroTtsProvider,
|
||||
System: SystemTtsProvider,
|
||||
Edge: EdgeTtsProvider,
|
||||
Novel: NovelTtsProvider,
|
||||
}
|
||||
let ttsProvider
|
||||
let ttsProviderName
|
||||
|
||||
async function onNarrateOneMessage() {
|
||||
audioElement.src = '/sounds/silence.mp3';
|
||||
const context = getContext();
|
||||
const id = $(this).closest('.mes').attr('mesid');
|
||||
const message = context.chat[id];
|
||||
@ -38,24 +85,6 @@ async function onNarrateOneMessage() {
|
||||
moduleWorker();
|
||||
}
|
||||
|
||||
let isWorkerBusy = false;
|
||||
|
||||
async function moduleWorkerWrapper() {
|
||||
// Don't touch me I'm busy...
|
||||
if (isWorkerBusy) {
|
||||
return;
|
||||
}
|
||||
|
||||
// I'm free. Let's update!
|
||||
try {
|
||||
isWorkerBusy = true;
|
||||
await moduleWorker();
|
||||
}
|
||||
finally {
|
||||
isWorkerBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function moduleWorker() {
|
||||
// Primarily determining when to add new chat to the TTS queue
|
||||
const enabled = $('#tts_enabled').is(':checked')
|
||||
@ -144,7 +173,7 @@ function resetTtsPlayback() {
|
||||
|
||||
// Reset audio element
|
||||
audioElement.currentTime = 0;
|
||||
audioElement.src = '';
|
||||
audioElement.src = '/sounds/silence.mp3';
|
||||
|
||||
// Clear any queue items
|
||||
ttsJobQueue.splice(0, ttsJobQueue.length);
|
||||
@ -191,6 +220,7 @@ window.debugTtsPlayback = debugTtsPlayback
|
||||
//##################//
|
||||
|
||||
let audioElement = new Audio()
|
||||
audioElement.autoplay = true
|
||||
|
||||
let audioJobQueue = []
|
||||
let currentAudioJob
|
||||
@ -220,7 +250,7 @@ async function playAudioData(audioBlob) {
|
||||
window['tts_preview'] = function (id) {
|
||||
const audio = document.getElementById(id)
|
||||
|
||||
if (!$(audio).data('disabled')) {
|
||||
if (audio && !$(audio).data('disabled')) {
|
||||
audio.play()
|
||||
}
|
||||
else {
|
||||
@ -241,7 +271,9 @@ async function onTtsVoicesClick() {
|
||||
<b class="voice_name">${voice.name}</b>
|
||||
<i onclick="tts_preview('${voice.voice_id}')" class="fa-solid fa-play"></i>
|
||||
</div>`
|
||||
popupText += `<audio id="${voice.voice_id}" src="${voice.preview_url}" data-disabled="${voice.preview_url == false}"></audio>`
|
||||
if (voice.preview_url) {
|
||||
popupText += `<audio id="${voice.voice_id}" src="${voice.preview_url}" data-disabled="${voice.preview_url == false}"></audio>`
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
popupText = 'Could not load voices list. Check your API key.'
|
||||
@ -267,6 +299,7 @@ function updateUiAudioPlayState() {
|
||||
}
|
||||
|
||||
function onAudioControlClicked() {
|
||||
audioElement.src = '/sounds/silence.mp3';
|
||||
let context = getContext()
|
||||
// Not pausing, doing a full stop to anything TTS is doing. Better UX as pause is not as useful
|
||||
if (!audioElement.paused || isTtsProcessing()) {
|
||||
@ -303,7 +336,7 @@ function completeCurrentAudioJob() {
|
||||
*/
|
||||
async function addAudioJob(response) {
|
||||
const audioData = await response.blob()
|
||||
if (!audioData.type in ['audio/mpeg', 'audio/wav', 'audio/x-wav', 'audio/wave']) {
|
||||
if (!audioData.type in ['audio/mpeg', 'audio/wav', 'audio/x-wav', 'audio/wave', 'audio/webm']) {
|
||||
throw `TTS received HTTP response with invalid data format. Expecting audio/mpeg, got ${audioData.type}`
|
||||
}
|
||||
audioJobQueue.push(audioData)
|
||||
@ -377,6 +410,13 @@ async function processTtsQueue() {
|
||||
console.log(`TTS: ${text}`)
|
||||
const char = currentTtsJob.name
|
||||
|
||||
// Remove character name from start of the line if power user setting is disabled
|
||||
if (char && !power_user.allow_name2_display) {
|
||||
debugger;
|
||||
const escapedChar = escapeRegex(char);
|
||||
text = text.replace(new RegExp(`^${escapedChar}:`, 'gm'), '');
|
||||
}
|
||||
|
||||
try {
|
||||
if (!text) {
|
||||
console.warn('Got empty text in TTS queue job.');
|
||||
@ -390,6 +430,7 @@ async function processTtsQueue() {
|
||||
const voice = await ttsProvider.getVoice((voiceMap[char]))
|
||||
const voiceId = voice.voice_id
|
||||
if (voiceId == null) {
|
||||
toastr.error(`Specified voice for ${char} was not found. Check the TTS extension settings.`)
|
||||
throw `Unable to attain voiceId for ${char}`
|
||||
}
|
||||
tts(text, voiceId)
|
||||
@ -470,7 +511,6 @@ async function voicemapIsValid(parsedVoiceMap) {
|
||||
|
||||
async function updateVoiceMap() {
|
||||
let isValidResult = false
|
||||
const context = getContext()
|
||||
|
||||
const value = $('#tts_voice_map').val()
|
||||
const parsedVoiceMap = parseVoiceMap(value)
|
||||
@ -661,6 +701,29 @@ $(document).ready(function () {
|
||||
loadSettings() // Depends on Extension Controls and loadTtsProvider
|
||||
loadTtsProvider(extension_settings.tts.currentProvider) // No dependencies
|
||||
addAudioControl() // Depends on Extension Controls
|
||||
setInterval(moduleWorkerWrapper, UPDATE_INTERVAL) // Init depends on all the things
|
||||
const wrapper = new ModuleWorkerWrapper(moduleWorker);
|
||||
setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL) // Init depends on all the things
|
||||
eventSource.on(event_types.MESSAGE_SWIPED, resetTtsPlayback);
|
||||
|
||||
// Mobiles need to "activate" the Audio element with click before it can be played
|
||||
if (isMobile()) {
|
||||
console.debug('Activating mobile audio element on first click');
|
||||
let audioActivated = false;
|
||||
|
||||
// Play silence on first click
|
||||
$(document).on('click touchend', function () {
|
||||
// Prevent multiple activations
|
||||
if (audioActivated) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.debug('Activating audio element...');
|
||||
audioActivated = true;
|
||||
audioElement.src = '/sounds/silence.mp3';
|
||||
// Reset volume to 1
|
||||
audioElement.onended = function () {
|
||||
console.debug('Audio element activated');
|
||||
};
|
||||
});
|
||||
}
|
||||
})
|
||||
|
@ -3,7 +3,8 @@
|
||||
"loading_order": 10,
|
||||
"requires": [],
|
||||
"optional": [
|
||||
"tts"
|
||||
"silero-tts",
|
||||
"edge-tts"
|
||||
],
|
||||
"js": "index.js",
|
||||
"css": "style.css",
|
||||
|
130
public/scripts/extensions/tts/novel.js
Normal file
130
public/scripts/extensions/tts/novel.js
Normal file
@ -0,0 +1,130 @@
|
||||
import { getRequestHeaders } from "../../../script.js"
|
||||
import { getPreviewString } from "./index.js"
|
||||
|
||||
export { NovelTtsProvider }
|
||||
|
||||
class NovelTtsProvider {
|
||||
//########//
|
||||
// Config //
|
||||
//########//
|
||||
|
||||
settings
|
||||
voices = []
|
||||
separator = ' . '
|
||||
audioElement = document.createElement('audio')
|
||||
|
||||
defaultSettings = {
|
||||
voiceMap: {}
|
||||
}
|
||||
|
||||
get settingsHtml() {
|
||||
let html = `Use NovelAI's TTS engine.<br>
|
||||
The Voice IDs in the preview list are only examples, as it can be any string of text. Feel free to try different options!<br>
|
||||
<small><i>Hint: Save an API key in the NovelAI API settings to use it here.</i></small>`;
|
||||
return html;
|
||||
}
|
||||
|
||||
onSettingsChange() {
|
||||
}
|
||||
|
||||
loadSettings(settings) {
|
||||
// Populate Provider UI given input settings
|
||||
if (Object.keys(settings).length == 0) {
|
||||
console.info("Using default TTS Provider settings")
|
||||
}
|
||||
|
||||
// Only accept keys defined in defaultSettings
|
||||
this.settings = this.defaultSettings
|
||||
|
||||
for (const key in settings) {
|
||||
if (key in this.settings) {
|
||||
this.settings[key] = settings[key]
|
||||
} else {
|
||||
throw `Invalid setting passed to TTS Provider: ${key}`
|
||||
}
|
||||
}
|
||||
|
||||
console.info("Settings loaded")
|
||||
}
|
||||
|
||||
|
||||
async onApplyClick() {
|
||||
return
|
||||
}
|
||||
|
||||
//#################//
|
||||
// TTS Interfaces //
|
||||
//#################//
|
||||
|
||||
async getVoice(voiceName) {
|
||||
if (!voiceName) {
|
||||
throw `TTS Voice name not provided`
|
||||
}
|
||||
|
||||
return { name: voiceName, voice_id: voiceName, lang: 'en-US', preview_url: false}
|
||||
}
|
||||
|
||||
async generateTts(text, voiceId) {
|
||||
const response = await this.fetchTtsGeneration(text, voiceId)
|
||||
return response
|
||||
}
|
||||
|
||||
//###########//
|
||||
// API CALLS //
|
||||
//###########//
|
||||
async fetchTtsVoiceIds() {
|
||||
const voices = [
|
||||
{ name: 'Ligeia', voice_id: 'Ligeia', lang: 'en-US', preview_url: false },
|
||||
{ name: 'Aini', voice_id: 'Aini', lang: 'en-US', preview_url: false },
|
||||
{ name: 'Orea', voice_id: 'Orea', lang: 'en-US', preview_url: false },
|
||||
{ name: 'Claea', voice_id: 'Claea', lang: 'en-US', preview_url: false },
|
||||
{ name: 'Lim', voice_id: 'Lim', lang: 'en-US', preview_url: false },
|
||||
{ name: 'Aurae', voice_id: 'Aurae', lang: 'en-US', preview_url: false },
|
||||
{ name: 'Naia', voice_id: 'Naia', lang: 'en-US', preview_url: false },
|
||||
{ name: 'Aulon', voice_id: 'Aulon', lang: 'en-US', preview_url: false },
|
||||
{ name: 'Elei', voice_id: 'Elei', lang: 'en-US', preview_url: false },
|
||||
{ name: 'Ogma', voice_id: 'Ogma', lang: 'en-US', preview_url: false },
|
||||
{ name: 'Raid', voice_id: 'Raid', lang: 'en-US', preview_url: false },
|
||||
{ name: 'Pega', voice_id: 'Pega', lang: 'en-US', preview_url: false },
|
||||
{ name: 'Lam', voice_id: 'Lam', lang: 'en-US', preview_url: false },
|
||||
];
|
||||
|
||||
return voices;
|
||||
}
|
||||
|
||||
|
||||
async previewTtsVoice(id) {
|
||||
this.audioElement.pause();
|
||||
this.audioElement.currentTime = 0;
|
||||
|
||||
const text = getPreviewString('en-US')
|
||||
const response = await this.fetchTtsGeneration(text, id)
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`)
|
||||
}
|
||||
|
||||
const audio = await response.blob();
|
||||
const url = URL.createObjectURL(audio);
|
||||
this.audioElement.src = url;
|
||||
this.audioElement.play();
|
||||
}
|
||||
|
||||
async fetchTtsGeneration(inputText, voiceId) {
|
||||
console.info(`Generating new TTS for voice_id ${voiceId}`)
|
||||
const response = await fetch(`/novel_tts`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({
|
||||
"text": inputText,
|
||||
"voice": voiceId,
|
||||
})
|
||||
}
|
||||
)
|
||||
if (!response.ok) {
|
||||
toastr.error(response.statusText, 'TTS Generation Failed');
|
||||
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
||||
}
|
||||
return response
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { getApiUrl, modules } from "../../extensions.js"
|
||||
import { doExtrasFetch, getApiUrl, modules } from "../../extensions.js"
|
||||
|
||||
export { SileroTtsProvider }
|
||||
|
||||
@ -21,7 +21,7 @@ class SileroTtsProvider {
|
||||
<label for="silero_tts_endpoint">Provider Endpoint:</label>
|
||||
<input id="silero_tts_endpoint" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.provider_endpoint}"/>
|
||||
<span>
|
||||
<span>Use <a target="_blank" href="https://github.com/Cohee1207/SillyTavern-extras">SillyTavern Extras API</a> or <a target="_blank" href="https://github.com/ouoertheo/silero-api-server">Silero TTS Server</a>.</span>
|
||||
<span>Use <a target="_blank" href="https://github.com/SillyTavern/SillyTavern-extras">SillyTavern Extras API</a> or <a target="_blank" href="https://github.com/ouoertheo/silero-api-server">Silero TTS Server</a>.</span>
|
||||
`
|
||||
return html
|
||||
}
|
||||
@ -50,7 +50,7 @@ class SileroTtsProvider {
|
||||
|
||||
const apiCheckInterval = setInterval(() => {
|
||||
// Use Extras API if TTS support is enabled
|
||||
if (modules.includes('tts')) {
|
||||
if (modules.includes('tts') || modules.includes('silero-tts')) {
|
||||
const baseUrl = new URL(getApiUrl());
|
||||
baseUrl.pathname = '/api/tts';
|
||||
this.settings.provider_endpoint = baseUrl.toString();
|
||||
@ -94,7 +94,7 @@ class SileroTtsProvider {
|
||||
// API CALLS //
|
||||
//###########//
|
||||
async fetchTtsVoiceIds() {
|
||||
const response = await fetch(`${this.settings.provider_endpoint}/speakers`)
|
||||
const response = await doExtrasFetch(`${this.settings.provider_endpoint}/speakers`)
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
|
||||
}
|
||||
@ -104,7 +104,7 @@ class SileroTtsProvider {
|
||||
|
||||
async fetchTtsGeneration(inputText, voiceId) {
|
||||
console.info(`Generating new TTS for voice_id ${voiceId}`)
|
||||
const response = await fetch(
|
||||
const response = await doExtrasFetch(
|
||||
`${this.settings.provider_endpoint}/generate`,
|
||||
{
|
||||
method: 'POST',
|
||||
@ -118,25 +118,15 @@ class SileroTtsProvider {
|
||||
}
|
||||
)
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
|
||||
toastr.error(response.statusText, 'TTS Generation Failed');
|
||||
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
// Interface not used by Silero TTS
|
||||
async fetchTtsFromHistory(history_item_id) {
|
||||
console.info(`Fetched existing TTS with history_item_id ${history_item_id}`)
|
||||
const response = await fetch(
|
||||
`https://api.elevenlabs.io/v1/history/${history_item_id}/audio`,
|
||||
{
|
||||
headers: {
|
||||
'xi-api-key': this.API_KEY
|
||||
}
|
||||
}
|
||||
)
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
|
||||
}
|
||||
return response
|
||||
return Promise.resolve(history_item_id);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,3 +1,6 @@
|
||||
import { isMobile } from "../../RossAscends-mods.js";
|
||||
import { getPreviewString } from "./index.js";
|
||||
|
||||
export { SystemTtsProvider }
|
||||
|
||||
/**
|
||||
@ -74,20 +77,6 @@ class SystemTtsProvider {
|
||||
// Config //
|
||||
//########//
|
||||
|
||||
previewStrings = {
|
||||
'en-US': 'The quick brown fox jumps over the lazy dog',
|
||||
'en-GB': 'Sphinx of black quartz, judge my vow',
|
||||
'fr-FR': 'Portez ce vieux whisky au juge blond qui fume',
|
||||
'de-DE': 'Victor jagt zwölf Boxkämpfer quer über den großen Sylter Deich',
|
||||
'it-IT': "Pranzo d'acqua fa volti sghembi",
|
||||
'es-ES': 'Quiere la boca exhausta vid, kiwi, piña y fugaz jamón',
|
||||
'es-MX': 'Fabio me exige, sin tapujos, que añada cerveza al whisky',
|
||||
'ru-RU': 'В чащах юга жил бы цитрус? Да, но фальшивый экземпляр!',
|
||||
'pt-BR': 'Vejo xá gritando que fez show sem playback.',
|
||||
'pt-PR': 'Todo pajé vulgar faz boquinha sexy com kiwi.',
|
||||
'uk-UA': "Фабрикуймо гідність, лящім їжею, ґав хапаймо, з'єднавці чаш!",
|
||||
}
|
||||
fallbackPreview = 'Neque porro quisquam est qui dolorem ipsum quia dolor sit amet'
|
||||
settings
|
||||
voices = []
|
||||
separator = ' ... '
|
||||
@ -124,6 +113,21 @@ class SystemTtsProvider {
|
||||
console.info("Using default TTS Provider settings");
|
||||
}
|
||||
|
||||
// iOS should only allows speech synthesis trigged by user interaction
|
||||
if (isMobile()) {
|
||||
let hasEnabledVoice = false;
|
||||
|
||||
document.addEventListener('click', () => {
|
||||
if (hasEnabledVoice) {
|
||||
return;
|
||||
}
|
||||
const utterance = new SpeechSynthesisUtterance('hi');
|
||||
utterance.volume = 0;
|
||||
speechSynthesis.speak(utterance);
|
||||
hasEnabledVoice = true;
|
||||
});
|
||||
}
|
||||
|
||||
// Only accept keys defined in defaultSettings
|
||||
this.settings = this.defaultSettings;
|
||||
|
||||
@ -172,7 +176,7 @@ class SystemTtsProvider {
|
||||
}
|
||||
|
||||
speechSynthesis.cancel();
|
||||
const text = this.previewStrings[voice.lang] ?? this.fallbackPreview;
|
||||
const text = getPreviewString(voice.lang);
|
||||
const utterance = new SpeechSynthesisUtterance(text);
|
||||
utterance.voice = voice;
|
||||
utterance.rate = 1;
|
||||
|
@ -2,10 +2,10 @@
|
||||
|
||||
export function SaveLocal(target, val) {
|
||||
localStorage.setItem(target, val);
|
||||
console.log('SaveLocal -- ' + target + ' : ' + val);
|
||||
console.debug('SaveLocal -- ' + target + ' : ' + val);
|
||||
}
|
||||
export function LoadLocal(target) {
|
||||
console.log('LoadLocal -- ' + target);
|
||||
console.debug('LoadLocal -- ' + target);
|
||||
return localStorage.getItem(target);
|
||||
|
||||
}
|
||||
|
2240
public/scripts/fuse.js
Normal file
2240
public/scripts/fuse.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -53,8 +53,11 @@ import {
|
||||
saveChatConditional,
|
||||
deactivateSendButtons,
|
||||
activateSendButtons,
|
||||
eventSource,
|
||||
event_types,
|
||||
getCurrentChatId,
|
||||
} from "../script.js";
|
||||
import { appendTagToList, createTagMapFromList, getTagsList, applyTagsOnCharacterSelect } from './tags.js';
|
||||
import { appendTagToList, createTagMapFromList, getTagsList, applyTagsOnCharacterSelect, tag_map } from './tags.js';
|
||||
|
||||
export {
|
||||
selected_group,
|
||||
@ -117,7 +120,7 @@ async function regenerateGroup() {
|
||||
break;
|
||||
}
|
||||
|
||||
deleteLastMessage();
|
||||
await deleteLastMessage();
|
||||
}
|
||||
|
||||
generateGroupWrapper();
|
||||
@ -172,17 +175,28 @@ export async function getGroupChat(groupId) {
|
||||
}
|
||||
|
||||
await saveGroupChat(groupId, true);
|
||||
eventSource.emit(event_types.CHAT_CHANGED, getCurrentChatId());
|
||||
}
|
||||
|
||||
function getFirstCharacterMessage(character) {
|
||||
let messageText = character.first_mes;
|
||||
|
||||
// if there are alternate greetings, pick one at random
|
||||
if (Array.isArray(character.data?.alternate_greetings)) {
|
||||
const messageTexts = [character.first_mes, ...character.data.alternate_greetings].filter(x => x);
|
||||
messageText = messageTexts[Math.floor(Math.random() * messageTexts.length)];
|
||||
}
|
||||
|
||||
const mes = {};
|
||||
mes["is_user"] = false;
|
||||
mes["is_system"] = false;
|
||||
mes["name"] = character.name;
|
||||
mes["is_name"] = true;
|
||||
mes["send_date"] = humanizedDateTime();
|
||||
mes["mes"] = character.first_mes
|
||||
? substituteParams(character.first_mes.trim(), name1, character.name)
|
||||
mes["original_avatar"] = character.avatar;
|
||||
mes["extra"] = { "gen_id": Date.now() * Math.random() * 1000000 };
|
||||
mes["mes"] = messageText
|
||||
? substituteParams(messageText.trim(), name1, character.name)
|
||||
: default_ch_mes;
|
||||
mes["force_avatar"] =
|
||||
character.avatar != "none"
|
||||
@ -382,16 +396,16 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_group_generating) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Auto-navigate back to group menu
|
||||
if (menu_type !== "group_edit") {
|
||||
select_group_chats(selected_group);
|
||||
await delay(1);
|
||||
}
|
||||
|
||||
if (is_group_generating) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const group = groups.find((x) => x.id === selected_group);
|
||||
let typingIndicator = $("#chat .typing_indicator");
|
||||
|
||||
@ -501,7 +515,7 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
|
||||
const bias = getBiasStrings(userInput);
|
||||
await sendMessageAsUser(userInput, bias.messageBias);
|
||||
await saveChatConditional();
|
||||
$('#send_textarea').val('');
|
||||
$('#send_textarea').val('').trigger('input');
|
||||
}
|
||||
|
||||
// now the real generation begins: cycle through every activated character
|
||||
@ -533,7 +547,7 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
|
||||
}
|
||||
|
||||
// if not swipe - check if message generated already
|
||||
if (type !== "swipe" && !isMultigenEnabled() && chat.length == messagesBefore) {
|
||||
if (generateType === "group_chat" && !isMultigenEnabled() && chat.length == messagesBefore) {
|
||||
await delay(100);
|
||||
}
|
||||
// if swipe - see if message changed
|
||||
@ -780,6 +794,7 @@ async function deleteGroup(id) {
|
||||
|
||||
if (response.ok) {
|
||||
selected_group = null;
|
||||
delete tag_map[id];
|
||||
resetChatState();
|
||||
clearChat();
|
||||
printMessages();
|
||||
@ -811,6 +826,8 @@ export async function editGroup(id, immediately, reload = true) {
|
||||
saveGroupDebounced(group);
|
||||
}
|
||||
|
||||
let groupAutoModeAbortController = null;
|
||||
|
||||
async function groupChatAutoModeWorker() {
|
||||
if (!is_group_automode_enabled || online_status === "no_connection") {
|
||||
return;
|
||||
@ -826,7 +843,8 @@ async function groupChatAutoModeWorker() {
|
||||
return;
|
||||
}
|
||||
|
||||
await generateGroupWrapper(true);
|
||||
groupAutoModeAbortController = new AbortController();
|
||||
await generateGroupWrapper(true, 'auto', { signal: groupAutoModeAbortController.signal });
|
||||
}
|
||||
|
||||
async function modifyGroupMember(chat_id, groupMember, isDelete) {
|
||||
@ -947,7 +965,7 @@ function select_group_chats(groupId, skipAnimation) {
|
||||
template.find(".avatar img").attr("title", character.avatar);
|
||||
template.find(".ch_name").text(character.name);
|
||||
template.attr("chid", characters.indexOf(character));
|
||||
template.addClass(character.fav == 'true' ? 'is_fav' : '');
|
||||
template.toggleClass('is_fav', character.fav || character.fav == 'true');
|
||||
|
||||
if (!group) {
|
||||
template.find('[data-action="speak"]').hide();
|
||||
@ -995,7 +1013,7 @@ function select_group_chats(groupId, skipAnimation) {
|
||||
}
|
||||
|
||||
$("#dialogue_popup").data("group_id", groupId);
|
||||
callPopup("<h3>Delete the group?</h3>", "del_group");
|
||||
callPopup('<h3>Delete the group?</h3><p>This will also delete all your chats with that group. If you want to delete a single conversation, select a "View past chats" option in the lower left menu.</p>', "del_group");
|
||||
});
|
||||
|
||||
updateFavButtonState(group?.fav ?? false);
|
||||
@ -1077,6 +1095,7 @@ function select_group_chats(groupId, skipAnimation) {
|
||||
}
|
||||
|
||||
sortGroupMembers("#rm_group_add_members .group_member");
|
||||
await eventSource.emit(event_types.GROUP_UPDATED);
|
||||
});
|
||||
}
|
||||
|
||||
@ -1399,6 +1418,23 @@ function onGroupScenarioRemoveClick() {
|
||||
$(this).closest('.group_scenario').find('.group_chat_scenario').val('').trigger('input');
|
||||
}
|
||||
|
||||
function onSendTextareaInput() {
|
||||
if (is_group_automode_enabled) {
|
||||
// Wait for current automode generation to finish
|
||||
is_group_automode_enabled = false;
|
||||
$("#rm_group_automode").prop("checked", false);
|
||||
}
|
||||
}
|
||||
|
||||
function stopAutoModeGeneration() {
|
||||
if (groupAutoModeAbortController) {
|
||||
groupAutoModeAbortController.abort();
|
||||
}
|
||||
|
||||
is_group_automode_enabled = false;
|
||||
$("#rm_group_automode").prop("checked", false);
|
||||
}
|
||||
|
||||
jQuery(() => {
|
||||
$(document).on("click", ".group_select", selectGroup);
|
||||
$(document).on("input", ".group_chat_scenario", onGroupScenarioInput);
|
||||
@ -1410,5 +1446,7 @@ jQuery(() => {
|
||||
$("#rm_group_automode").on("input", function () {
|
||||
const value = $(this).prop("checked");
|
||||
is_group_automode_enabled = value;
|
||||
eventSource.once(event_types.GENERATION_STOPPED, stopAutoModeGeneration);
|
||||
});
|
||||
$("#send_textarea").on("keyup", onSendTextareaInput);
|
||||
});
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {
|
||||
getRequestHeaders,
|
||||
saveSettingsDebounced,
|
||||
getStoppingStrings,
|
||||
} from "../script.js";
|
||||
@ -9,6 +10,7 @@ export {
|
||||
formatKoboldUrl,
|
||||
getKoboldGenerationData,
|
||||
canUseKoboldStopSequence,
|
||||
canUseKoboldStreaming,
|
||||
};
|
||||
|
||||
const kai_settings = {
|
||||
@ -23,9 +25,11 @@ const kai_settings = {
|
||||
rep_pen_slope: 0.9,
|
||||
single_line: false,
|
||||
use_stop_sequence: false,
|
||||
streaming_kobold: false,
|
||||
};
|
||||
|
||||
const MIN_STOP_SEQUENCE_VERSION = '1.2.2';
|
||||
const MIN_STREAMING_KCPPVERSION = '1.30';
|
||||
|
||||
function formatKoboldUrl(value) {
|
||||
try {
|
||||
@ -58,6 +62,10 @@ function loadKoboldSettings(preset) {
|
||||
kai_settings.single_line = preset.single_line;
|
||||
$('#single_line').prop('checked', kai_settings.single_line);
|
||||
}
|
||||
if (preset.hasOwnProperty('streaming_kobold')) {
|
||||
kai_settings.streaming_kobold = preset.streaming_kobold;
|
||||
$('#streaming_kobold').prop('checked', kai_settings.streaming_kobold);
|
||||
}
|
||||
}
|
||||
|
||||
function getKoboldGenerationData(finalPromt, this_settings, this_amount_gen, this_max_context, isImpersonate) {
|
||||
@ -86,10 +94,54 @@ function getKoboldGenerationData(finalPromt, this_settings, this_amount_gen, thi
|
||||
use_world_info: false,
|
||||
singleline: kai_settings.single_line,
|
||||
stop_sequence: kai_settings.use_stop_sequence ? getStoppingStrings(isImpersonate, false) : undefined,
|
||||
streaming: kai_settings.streaming_kobold && kai_settings.can_use_streaming,
|
||||
can_abort: kai_settings.can_use_streaming,
|
||||
};
|
||||
return generate_data;
|
||||
}
|
||||
|
||||
export async function generateKoboldWithStreaming(generate_data, signal) {
|
||||
const response = await fetch('/generate', {
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify(generate_data),
|
||||
method: 'POST',
|
||||
signal: signal,
|
||||
});
|
||||
|
||||
return async function* streamData() {
|
||||
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);
|
||||
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)
|
||||
messageBuffer += response;
|
||||
eventList = messageBuffer.split("\n\n");
|
||||
// Last element will be an empty string or a leftover partial message
|
||||
messageBuffer = eventList.pop();
|
||||
|
||||
for (let event of eventList) {
|
||||
for (let subEvent of event.split('\n')) {
|
||||
if (subEvent.startsWith("data")) {
|
||||
let data = JSON.parse(subEvent.substring(5));
|
||||
getMessage += (data?.token || '');
|
||||
yield getMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (done) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sliders = [
|
||||
{
|
||||
name: "temp",
|
||||
@ -160,6 +212,12 @@ function canUseKoboldStopSequence(version) {
|
||||
return (version || '0.0.0').localeCompare(MIN_STOP_SEQUENCE_VERSION, undefined, { numeric: true, sensitivity: 'base' }) > -1;
|
||||
}
|
||||
|
||||
function canUseKoboldStreaming(koboldVersion) {
|
||||
if (koboldVersion.result == 'KoboldCpp') {
|
||||
return (koboldVersion.version || '0.0').localeCompare(MIN_STREAMING_KCPPVERSION, undefined, { numeric: true, sensitivity: 'base' }) > -1;
|
||||
} else return false;
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
sliders.forEach(slider => {
|
||||
$(document).on("input", slider.sliderId, function () {
|
||||
@ -176,4 +234,10 @@ $(document).ready(function () {
|
||||
kai_settings.single_line = value;
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#streaming_kobold').on("input", function () {
|
||||
const value = $(this).prop('checked');
|
||||
kai_settings.streaming_kobold = value;
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
});
|
@ -1,4 +1,5 @@
|
||||
import {
|
||||
getRequestHeaders,
|
||||
saveSettingsDebounced,
|
||||
} from "../script.js";
|
||||
|
||||
@ -19,6 +20,7 @@ const nai_settings = {
|
||||
tail_free_sampling_novel: 0.68,
|
||||
model_novel: "euterpe-v2",
|
||||
preset_settings_novel: "Classic-Euterpe",
|
||||
streaming_novel: false,
|
||||
};
|
||||
|
||||
const nai_tiers = {
|
||||
@ -65,6 +67,7 @@ function loadNovelSettings(settings) {
|
||||
nai_settings.rep_pen_freq_novel = settings.rep_pen_freq_novel;
|
||||
nai_settings.rep_pen_presence_novel = settings.rep_pen_presence_novel;
|
||||
nai_settings.tail_free_sampling_novel = settings.tail_free_sampling_novel;
|
||||
nai_settings.streaming_novel = !!settings.streaming_novel;
|
||||
loadNovelSettingsUi(nai_settings);
|
||||
}
|
||||
|
||||
@ -83,6 +86,7 @@ function loadNovelSettingsUi(ui_settings) {
|
||||
$("#rep_pen_presence_counter_novel").text(Number(ui_settings.rep_pen_presence_novel).toFixed(3));
|
||||
$("#tail_free_sampling_novel").val(ui_settings.tail_free_sampling_novel);
|
||||
$("#tail_free_sampling_counter_novel").text(Number(ui_settings.tail_free_sampling_novel).toFixed(3));
|
||||
$("#streaming_novel").prop('checked', ui_settings.streaming_novel);
|
||||
}
|
||||
|
||||
const sliders = [
|
||||
@ -150,15 +154,58 @@ export function getNovelGenerationData(finalPromt, this_settings, this_amount_ge
|
||||
"typical_p": this_settings.typical_p,
|
||||
//"stop_sequences": {{187}},
|
||||
//bad_words_ids = {{50256}, {0}, {1}};
|
||||
//generate_until_sentence = true;
|
||||
"generate_until_sentence": true,
|
||||
"use_cache": false,
|
||||
//use_string = true;
|
||||
"use_string": true,
|
||||
"return_full_text": false,
|
||||
"prefix": "vanilla",
|
||||
"order": this_settings.order
|
||||
"order": this_settings.order,
|
||||
"streaming": nai_settings.streaming_novel,
|
||||
};
|
||||
}
|
||||
|
||||
export async function generateNovelWithStreaming(generate_data, signal) {
|
||||
const response = await fetch('/generate_novelai', {
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify(generate_data),
|
||||
method: 'POST',
|
||||
signal: signal,
|
||||
});
|
||||
|
||||
return async function* streamData() {
|
||||
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);
|
||||
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)
|
||||
messageBuffer += response;
|
||||
eventList = messageBuffer.split("\n\n");
|
||||
// Last element will be an empty string or a leftover partial message
|
||||
messageBuffer = eventList.pop();
|
||||
|
||||
for (let event of eventList) {
|
||||
for (let subEvent of event.split('\n')) {
|
||||
if (subEvent.startsWith("data")) {
|
||||
let data = JSON.parse(subEvent.substring(5));
|
||||
getMessage += (data?.token || '');
|
||||
yield getMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (done) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
sliders.forEach(slider => {
|
||||
$(document).on("input", slider.sliderId, function () {
|
||||
@ -171,6 +218,12 @@ $(document).ready(function () {
|
||||
});
|
||||
});
|
||||
|
||||
$('#streaming_novel').on('input', function () {
|
||||
const value = !!$(this).prop('checked');
|
||||
nai_settings.streaming_novel = value;
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$("#model_novel_select").change(function () {
|
||||
nai_settings.model_novel = $("#model_novel_select").find(":selected").val();
|
||||
saveSettingsDebounced();
|
||||
|
@ -1,18 +0,0 @@
|
||||
async function loadNotes(file) {
|
||||
const toc = [];
|
||||
let hash = location.hash;
|
||||
let converter = new showdown.Converter({ tables: true, extensions: [showdownToc({ toc })] } );
|
||||
let text = await (await fetch(file)).text();
|
||||
let content = document.getElementById('content');
|
||||
content.innerHTML = converter.makeHtml(text);
|
||||
|
||||
if (hash) {
|
||||
const link = document.createElement('a');
|
||||
link.href = hash;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
}
|
||||
|
||||
window.loadNotes = loadNotes;
|
@ -19,6 +19,7 @@ import {
|
||||
getRequestHeaders,
|
||||
system_message_types,
|
||||
replaceBiasMarkup,
|
||||
is_send_press,
|
||||
} from "../script.js";
|
||||
import { groups, selected_group } from "./group-chats.js";
|
||||
|
||||
@ -79,25 +80,39 @@ const default_bias_presets = {
|
||||
};
|
||||
|
||||
const gpt3_max = 4095;
|
||||
const gpt3_16k_max = 16383;
|
||||
const gpt4_max = 8191;
|
||||
const gpt_neox_max = 2048;
|
||||
const gpt4_32k_max = 32767;
|
||||
const claude_max = 7500;
|
||||
const claude_100k_max = 99000;
|
||||
const unlocked_max = 100 * 1024;
|
||||
const oai_max_temp = 2.0;
|
||||
const claude_max_temp = 1.0;
|
||||
|
||||
let biasCache = undefined;
|
||||
const tokenCache = {};
|
||||
|
||||
export const chat_completion_sources = {
|
||||
OPENAI: 'openai',
|
||||
WINDOWAI: 'windowai',
|
||||
CLAUDE: 'claude',
|
||||
};
|
||||
|
||||
const default_settings = {
|
||||
preset_settings_openai: 'Default',
|
||||
temp_openai: 0.9,
|
||||
freq_pen_openai: 0.7,
|
||||
pres_pen_openai: 0.7,
|
||||
top_p_openai: 1.0,
|
||||
top_k_openai: 0,
|
||||
stream_openai: false,
|
||||
openai_max_context: gpt3_max,
|
||||
openai_max_tokens: 300,
|
||||
nsfw_toggle: true,
|
||||
enhance_definitions: false,
|
||||
wrap_in_quotes: false,
|
||||
send_if_empty: '',
|
||||
nsfw_first: false,
|
||||
main_prompt: default_main_prompt,
|
||||
nsfw_prompt: default_nsfw_prompt,
|
||||
@ -108,10 +123,12 @@ const default_settings = {
|
||||
bias_presets: default_bias_presets,
|
||||
wi_format: default_wi_format,
|
||||
openai_model: 'gpt-3.5-turbo',
|
||||
claude_model: 'claude-instant-v1',
|
||||
windowai_model: '',
|
||||
jailbreak_system: false,
|
||||
reverse_proxy: '',
|
||||
legacy_streaming: false,
|
||||
use_window_ai: false,
|
||||
chat_completion_source: chat_completion_sources.OPENAI,
|
||||
max_context_unlocked: false,
|
||||
};
|
||||
|
||||
@ -121,12 +138,14 @@ const oai_settings = {
|
||||
freq_pen_openai: 0,
|
||||
pres_pen_openai: 0,
|
||||
top_p_openai: 1.0,
|
||||
top_k_openai: 0,
|
||||
stream_openai: false,
|
||||
openai_max_context: gpt3_max,
|
||||
openai_max_tokens: 300,
|
||||
nsfw_toggle: true,
|
||||
enhance_definitions: false,
|
||||
wrap_in_quotes: false,
|
||||
send_if_empty: '',
|
||||
nsfw_first: false,
|
||||
main_prompt: default_main_prompt,
|
||||
nsfw_prompt: default_nsfw_prompt,
|
||||
@ -137,10 +156,12 @@ const oai_settings = {
|
||||
bias_presets: default_bias_presets,
|
||||
wi_format: default_wi_format,
|
||||
openai_model: 'gpt-3.5-turbo',
|
||||
claude_model: 'claude-instant-v1',
|
||||
windowai_model: '',
|
||||
jailbreak_system: false,
|
||||
reverse_proxy: '',
|
||||
legacy_streaming: false,
|
||||
use_window_ai: false,
|
||||
chat_completion_source: chat_completion_sources.OPENAI,
|
||||
max_context_unlocked: false,
|
||||
};
|
||||
|
||||
@ -298,7 +319,7 @@ function formatWorldInfo(value) {
|
||||
return stringFormat(oai_settings.wi_format, value);
|
||||
}
|
||||
|
||||
async function prepareOpenAIMessages(name2, storyString, worldInfoBefore, worldInfoAfter, extensionPrompt, bias, type, quietPrompt) {
|
||||
async function prepareOpenAIMessages({ systemPrompt, name2, storyString, worldInfoBefore, worldInfoAfter, extensionPrompt, bias, type, quietPrompt, jailbreakPrompt } = {}) {
|
||||
const isImpersonate = type == "impersonate";
|
||||
let this_max_context = oai_settings.openai_max_context;
|
||||
let enhance_definitions_prompt = "";
|
||||
@ -312,7 +333,7 @@ async function prepareOpenAIMessages(name2, storyString, worldInfoBefore, worldI
|
||||
const wiBefore = formatWorldInfo(worldInfoBefore);
|
||||
const wiAfter = formatWorldInfo(worldInfoAfter);
|
||||
|
||||
let whole_prompt = getSystemPrompt(nsfw_toggle_prompt, enhance_definitions_prompt, wiBefore, storyString, wiAfter, extensionPrompt, isImpersonate);
|
||||
let whole_prompt = getSystemPrompt(systemPrompt, nsfw_toggle_prompt, enhance_definitions_prompt, wiBefore, storyString, wiAfter, extensionPrompt, isImpersonate);
|
||||
|
||||
// Join by a space and replace placeholders with real user/char names
|
||||
storyString = substituteParams(whole_prompt.join("\n")).replace(/\r/gm, '').trim();
|
||||
@ -342,7 +363,7 @@ async function prepareOpenAIMessages(name2, storyString, worldInfoBefore, worldI
|
||||
const groupMembers = groups.find(x => x.id === selected_group)?.members;
|
||||
let names = '';
|
||||
if (Array.isArray(groupMembers)) {
|
||||
names = groupMembers.map(member => characters.find(c => c.avatar === member)).map((x) => x.name);
|
||||
names = groupMembers.map(member => characters.find(c => c.avatar === member)).filter(x => x).map(x => x.name);
|
||||
names = names.join(', ')
|
||||
}
|
||||
new_chat_msg.content = `[Start a new group chat. Group members: ${names}]`;
|
||||
@ -362,8 +383,9 @@ async function prepareOpenAIMessages(name2, storyString, worldInfoBefore, worldI
|
||||
total_count += start_chat_count;
|
||||
}
|
||||
|
||||
if (oai_settings.jailbreak_system && oai_settings.jailbreak_prompt) {
|
||||
const jailbreakMessage = { "role": "system", "content": substituteParams(oai_settings.jailbreak_prompt) };
|
||||
const jailbreak = power_user.prefer_character_jailbreak && jailbreakPrompt ? jailbreakPrompt : oai_settings.jailbreak_prompt;
|
||||
if (oai_settings.jailbreak_system && jailbreak) {
|
||||
const jailbreakMessage = { "role": "system", "content": substituteParams(jailbreak) };
|
||||
openai_msgs.push(jailbreakMessage);
|
||||
|
||||
total_count += handler_instance.count([jailbreakMessage], true, 'jailbreak');
|
||||
@ -476,7 +498,9 @@ async function prepareOpenAIMessages(name2, storyString, worldInfoBefore, worldI
|
||||
];
|
||||
}
|
||||
|
||||
function getSystemPrompt(nsfw_toggle_prompt, enhance_definitions_prompt, wiBefore, storyString, wiAfter, extensionPrompt, isImpersonate) {
|
||||
function getSystemPrompt(systemPrompt, nsfw_toggle_prompt, enhance_definitions_prompt, wiBefore, storyString, wiAfter, extensionPrompt, isImpersonate) {
|
||||
// If the character has a custom system prompt AND user has it preferred, use that instead of the default
|
||||
let prompt = power_user.prefer_character_prompt && systemPrompt ? systemPrompt : oai_settings.main_prompt;
|
||||
let whole_prompt = [];
|
||||
|
||||
if (isImpersonate) {
|
||||
@ -485,10 +509,10 @@ function getSystemPrompt(nsfw_toggle_prompt, enhance_definitions_prompt, wiBefor
|
||||
else {
|
||||
// If it's toggled, NSFW prompt goes first.
|
||||
if (oai_settings.nsfw_first) {
|
||||
whole_prompt = [nsfw_toggle_prompt, oai_settings.main_prompt, enhance_definitions_prompt + "\n\n" + wiBefore, storyString, wiAfter, extensionPrompt];
|
||||
whole_prompt = [nsfw_toggle_prompt, prompt, enhance_definitions_prompt + "\n\n" + wiBefore, storyString, wiAfter, extensionPrompt];
|
||||
}
|
||||
else {
|
||||
whole_prompt = [oai_settings.main_prompt, nsfw_toggle_prompt, enhance_definitions_prompt, "\n", wiBefore, storyString, wiAfter, extensionPrompt].filter(elem => elem);
|
||||
whole_prompt = [prompt, nsfw_toggle_prompt, enhance_definitions_prompt, "\n", wiBefore, storyString, wiAfter, extensionPrompt].filter(elem => elem);
|
||||
}
|
||||
}
|
||||
return whole_prompt;
|
||||
@ -505,6 +529,7 @@ function tryParseStreamingError(str) {
|
||||
checkQuotaError(data);
|
||||
|
||||
if (data.error) {
|
||||
toastr.error(response.statusText, 'API returned an error');
|
||||
throw new Error(data);
|
||||
}
|
||||
}
|
||||
@ -538,6 +563,14 @@ async function sendWindowAIRequest(openai_msgs_tosend, signal, stream) {
|
||||
let lastContent = '';
|
||||
let finished = false;
|
||||
|
||||
const currentModel = await window.ai.getCurrentModel();
|
||||
let temperature = parseFloat(oai_settings.temp_openai);
|
||||
|
||||
if (currentModel.includes('claude') && temperature > claude_max_temp) {
|
||||
console.warn(`Claude model only supports temperature up to ${claude_max_temp}. Clamping ${temperature} to ${claude_max_temp}.`);
|
||||
temperature = claude_max_temp;
|
||||
}
|
||||
|
||||
async function* windowStreamingFunction() {
|
||||
while (true) {
|
||||
if (signal.aborted) {
|
||||
@ -561,7 +594,7 @@ async function sendWindowAIRequest(openai_msgs_tosend, signal, stream) {
|
||||
|
||||
const onStreamResult = (res, err) => {
|
||||
if (err) {
|
||||
handleWindowError(err);
|
||||
return;
|
||||
}
|
||||
|
||||
const thisContent = res?.message?.content;
|
||||
@ -579,8 +612,9 @@ async function sendWindowAIRequest(openai_msgs_tosend, signal, stream) {
|
||||
messages: openai_msgs_tosend,
|
||||
},
|
||||
{
|
||||
temperature: parseFloat(oai_settings.temp_openai),
|
||||
temperature: temperature,
|
||||
maxTokens: oai_settings.openai_max_tokens,
|
||||
model: oai_settings.windowai_model || null,
|
||||
onStreamResult: onStreamResult,
|
||||
}
|
||||
);
|
||||
@ -593,9 +627,9 @@ async function sendWindowAIRequest(openai_msgs_tosend, signal, stream) {
|
||||
resolve && resolve(content);
|
||||
})
|
||||
.catch((err) => {
|
||||
handleWindowError(err);
|
||||
finished = true;
|
||||
reject && reject(err);
|
||||
handleWindowError(err);
|
||||
});
|
||||
};
|
||||
|
||||
@ -624,32 +658,37 @@ async function sendOpenAIRequest(type, openai_msgs_tosend, signal) {
|
||||
}
|
||||
|
||||
let logit_bias = {};
|
||||
const isClaude = oai_settings.chat_completion_source == chat_completion_sources.CLAUDE;
|
||||
const stream = type !== 'quiet' && oai_settings.stream_openai;
|
||||
|
||||
// If we're using the window.ai extension, use that instead
|
||||
// Doesn't support logit bias yet
|
||||
if (oai_settings.use_window_ai) {
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.WINDOWAI) {
|
||||
return sendWindowAIRequest(openai_msgs_tosend, signal, stream);
|
||||
}
|
||||
|
||||
if (oai_settings.bias_preset_selected
|
||||
&& !isClaude // Claude doesn't support logit bias
|
||||
&& Array.isArray(oai_settings.bias_presets[oai_settings.bias_preset_selected])
|
||||
&& oai_settings.bias_presets[oai_settings.bias_preset_selected].length) {
|
||||
logit_bias = biasCache || await calculateLogitBias();
|
||||
biasCache = logit_bias;
|
||||
}
|
||||
|
||||
const model = isClaude ? oai_settings.claude_model : oai_settings.openai_model;
|
||||
const generate_data = {
|
||||
"messages": openai_msgs_tosend,
|
||||
"model": oai_settings.openai_model,
|
||||
"model": model,
|
||||
"temperature": parseFloat(oai_settings.temp_openai),
|
||||
"frequency_penalty": parseFloat(oai_settings.freq_pen_openai),
|
||||
"presence_penalty": parseFloat(oai_settings.pres_pen_openai),
|
||||
"top_p": parseFloat(oai_settings.top_p_openai),
|
||||
"top_k": parseFloat(oai_settings.top_k_openai),
|
||||
"max_tokens": oai_settings.openai_max_tokens,
|
||||
"stream": stream,
|
||||
"reverse_proxy": oai_settings.reverse_proxy,
|
||||
"logit_bias": logit_bias,
|
||||
"use_claude": isClaude,
|
||||
};
|
||||
|
||||
const generate_url = '/generate_openai';
|
||||
@ -670,6 +709,11 @@ async function sendOpenAIRequest(type, openai_msgs_tosend, signal) {
|
||||
const { done, value } = await reader.read();
|
||||
let response = decoder.decode(value);
|
||||
|
||||
// Claude's streaming SSE messages are separated by \r
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) {
|
||||
response = response.replace(/\r/g, "");
|
||||
}
|
||||
|
||||
tryParseStreamingError(response);
|
||||
|
||||
let eventList = [];
|
||||
@ -693,7 +737,7 @@ async function sendOpenAIRequest(type, openai_msgs_tosend, signal) {
|
||||
}
|
||||
let data = JSON.parse(event.substring(6));
|
||||
// the first and last messages are undefined, protect against that
|
||||
getMessage += data.choices[0]["delta"]["content"] || "";
|
||||
getMessage = getStreamingReply(getMessage, data);
|
||||
yield getMessage;
|
||||
}
|
||||
|
||||
@ -709,6 +753,7 @@ async function sendOpenAIRequest(type, openai_msgs_tosend, signal) {
|
||||
checkQuotaError(data);
|
||||
|
||||
if (data.error) {
|
||||
toastr.error(response.statusText, 'API returned an error');
|
||||
throw new Error(data);
|
||||
}
|
||||
|
||||
@ -716,6 +761,15 @@ async function sendOpenAIRequest(type, openai_msgs_tosend, signal) {
|
||||
}
|
||||
}
|
||||
|
||||
function getStreamingReply(getMessage, data) {
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) {
|
||||
getMessage = data.completion || "";
|
||||
} else{
|
||||
getMessage += data.choices[0]["delta"]["content"] || "";
|
||||
}
|
||||
return getMessage;
|
||||
}
|
||||
|
||||
function handleWindowError(err) {
|
||||
const text = parseWindowError(err);
|
||||
toastr.error(text, 'Window.ai returned an error');
|
||||
@ -833,10 +887,12 @@ function countTokens(messages, full = false) {
|
||||
token_count += cachedCount;
|
||||
}
|
||||
else {
|
||||
let model = getTokenizerModel();
|
||||
|
||||
jQuery.ajax({
|
||||
async: false,
|
||||
type: 'POST', //
|
||||
url: `/tokenize_openai?model=${oai_settings.openai_model}`,
|
||||
url: `/tokenize_openai?model=${model}`,
|
||||
data: JSON.stringify([message]),
|
||||
dataType: "json",
|
||||
contentType: "application/json",
|
||||
@ -853,6 +909,38 @@ function countTokens(messages, full = false) {
|
||||
return token_count;
|
||||
}
|
||||
|
||||
function getTokenizerModel() {
|
||||
// OpenAI models always provide their own tokenizer
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.OPENAI) {
|
||||
return oai_settings.openai_model;
|
||||
}
|
||||
|
||||
const turboTokenizer = 'gpt-3.5-turbo'
|
||||
// Select correct tokenizer for WindowAI proxies
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.WINDOWAI) {
|
||||
if (oai_settings.windowai_model.includes('gpt-4')) {
|
||||
return 'gpt-4';
|
||||
}
|
||||
else if (oai_settings.windowai_model.includes('gpt-3.5-turbo')) {
|
||||
return turboTokenizer;
|
||||
}
|
||||
else if (oai_settings.windowai_model.includes('claude')) {
|
||||
return turboTokenizer;
|
||||
}
|
||||
else if (oai_settings.windowai_model.includes('GPT-NeoXT')) {
|
||||
return 'gpt2';
|
||||
}
|
||||
}
|
||||
|
||||
// We don't have a Claude tokenizer for JS yet. Turbo 3.5 should be able to handle this.
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) {
|
||||
return turboTokenizer;
|
||||
}
|
||||
|
||||
// Default to Turbo 3.5
|
||||
return turboTokenizer;
|
||||
}
|
||||
|
||||
function loadOpenAISettings(data, settings) {
|
||||
openai_setting_names = data.openai_setting_names;
|
||||
openai_settings = data.openai_settings;
|
||||
@ -876,16 +964,20 @@ function loadOpenAISettings(data, settings) {
|
||||
oai_settings.freq_pen_openai = settings.freq_pen_openai ?? default_settings.freq_pen_openai;
|
||||
oai_settings.pres_pen_openai = settings.pres_pen_openai ?? default_settings.pres_pen_openai;
|
||||
oai_settings.top_p_openai = settings.top_p_openai ?? default_settings.top_p_openai;
|
||||
oai_settings.top_k_openai = settings.top_k_openai ?? default_settings.top_k_openai;
|
||||
oai_settings.stream_openai = settings.stream_openai ?? default_settings.stream_openai;
|
||||
oai_settings.openai_max_context = settings.openai_max_context ?? default_settings.openai_max_context;
|
||||
oai_settings.openai_max_tokens = settings.openai_max_tokens ?? default_settings.openai_max_tokens;
|
||||
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;
|
||||
oai_settings.use_window_ai = settings.use_window_ai ?? default_settings.use_window_ai;
|
||||
oai_settings.max_context_unlocked = settings.max_context_unlocked ?? default_settings.max_context_unlocked;
|
||||
oai_settings.nsfw_avoidance_prompt = settings.nsfw_avoidance_prompt ?? default_settings.nsfw_avoidance_prompt;
|
||||
oai_settings.send_if_empty = settings.send_if_empty ?? default_settings.send_if_empty;
|
||||
oai_settings.wi_format = settings.wi_format ?? default_settings.wi_format;
|
||||
oai_settings.claude_model = settings.claude_model ?? default_settings.claude_model;
|
||||
oai_settings.windowai_model = settings.windowai_model ?? default_settings.windowai_model;
|
||||
oai_settings.chat_completion_source = settings.chat_completion_source ?? default_settings.chat_completion_source;
|
||||
|
||||
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;
|
||||
@ -897,7 +989,9 @@ function loadOpenAISettings(data, settings) {
|
||||
|
||||
$('#stream_toggle').prop('checked', oai_settings.stream_openai);
|
||||
|
||||
$(`#model_openai_select option[value="${oai_settings.openai_model}"`).attr('selected', true).trigger('change');
|
||||
$(`#model_openai_select option[value="${oai_settings.openai_model}"`).attr('selected', true);
|
||||
$(`#model_claude_select option[value="${oai_settings.claude_model}"`).attr('selected', true);
|
||||
$(`#model_windowai_select option[value="${oai_settings.windowai_model}"`).attr('selected', true);
|
||||
$('#openai_max_context').val(oai_settings.openai_max_context);
|
||||
$('#openai_max_context_counter').text(`${oai_settings.openai_max_context}`);
|
||||
|
||||
@ -921,6 +1015,7 @@ function loadOpenAISettings(data, settings) {
|
||||
$('#impersonation_prompt_textarea').val(oai_settings.impersonation_prompt);
|
||||
$('#nsfw_avoidance_prompt_textarea').val(oai_settings.nsfw_avoidance_prompt);
|
||||
$('#wi_format_textarea').val(oai_settings.wi_format);
|
||||
$('#send_if_empty_textarea').val(oai_settings.send_if_empty);
|
||||
|
||||
$('#temp_openai').val(oai_settings.temp_openai);
|
||||
$('#temp_counter_openai').text(Number(oai_settings.temp_openai).toFixed(2));
|
||||
@ -934,6 +1029,9 @@ function loadOpenAISettings(data, settings) {
|
||||
$('#top_p_openai').val(oai_settings.top_p_openai);
|
||||
$('#top_p_counter_openai').text(Number(oai_settings.top_p_openai).toFixed(2));
|
||||
|
||||
$('#top_k_openai').val(oai_settings.top_k_openai);
|
||||
$('#top_k_counter_openai').text(Number(oai_settings.top_k_openai).toFixed(0));
|
||||
|
||||
if (settings.reverse_proxy !== undefined) oai_settings.reverse_proxy = settings.reverse_proxy;
|
||||
$('#openai_reverse_proxy').val(oai_settings.reverse_proxy);
|
||||
|
||||
@ -951,14 +1049,13 @@ function loadOpenAISettings(data, settings) {
|
||||
}
|
||||
$('#openai_logit_bias_preset').trigger('change');
|
||||
|
||||
$('#use_window_ai').prop('checked', oai_settings.use_window_ai);
|
||||
$('#chat_completion_source').val(oai_settings.chat_completion_source).trigger('change');
|
||||
$('#oai_max_context_unlocked').prop('checked', oai_settings.max_context_unlocked);
|
||||
$('#openai_form').toggle(!oai_settings.use_window_ai);
|
||||
}
|
||||
|
||||
async function getStatusOpen() {
|
||||
if (is_get_status_openai) {
|
||||
if (oai_settings.use_window_ai) {
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.WINDOWAI) {
|
||||
let status;
|
||||
|
||||
if ('ai' in window) {
|
||||
@ -973,6 +1070,12 @@ async function getStatusOpen() {
|
||||
return resultCheckStatusOpen();
|
||||
}
|
||||
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) {
|
||||
let status = 'Unable to verify key; press "Test Message" to validate.';
|
||||
setOnlineStatus(status);
|
||||
return resultCheckStatusOpen();
|
||||
}
|
||||
|
||||
let data = {
|
||||
reverse_proxy: oai_settings.reverse_proxy,
|
||||
};
|
||||
@ -1041,16 +1144,21 @@ function trySelectPresetByName(name) {
|
||||
|
||||
async function saveOpenAIPreset(name, settings) {
|
||||
const presetBody = {
|
||||
chat_completion_source: settings.chat_completion_source,
|
||||
openai_model: settings.openai_model,
|
||||
claude_model: settings.claude_model,
|
||||
windowai_model: settings.windowai_model,
|
||||
temperature: settings.temp_openai,
|
||||
frequency_penalty: settings.freq_pen_openai,
|
||||
presence_penalty: settings.pres_pen_openai,
|
||||
top_p: settings.top_p_openai,
|
||||
top_k: settings.top_k_openai,
|
||||
openai_max_context: settings.openai_max_context,
|
||||
openai_max_tokens: settings.openai_max_tokens,
|
||||
nsfw_toggle: settings.nsfw_toggle,
|
||||
enhance_definitions: settings.enhance_definitions,
|
||||
wrap_in_quotes: settings.wrap_in_quotes,
|
||||
send_if_empty: settings.send_if_empty,
|
||||
nsfw_first: settings.nsfw_first,
|
||||
main_prompt: settings.main_prompt,
|
||||
nsfw_prompt: settings.nsfw_prompt,
|
||||
@ -1310,17 +1418,22 @@ function onSettingsPresetChange() {
|
||||
const updateCheckbox = (selector, value) => $(selector).prop('checked', value).trigger('input');
|
||||
|
||||
const settingsToUpdate = {
|
||||
chat_completion_source: ['#chat_completion_source', 'chat_completion_source', false],
|
||||
temperature: ['#temp_openai', 'temp_openai', false],
|
||||
frequency_penalty: ['#freq_pen_openai', 'freq_pen_openai', false],
|
||||
presence_penalty: ['#pres_pen_openai', 'pres_pen_openai', false],
|
||||
top_p: ['#top_p_openai', 'top_p_openai', false],
|
||||
top_k: ['#top_k_openai', 'top_k_openai', false],
|
||||
max_context_unlocked: ['#oai_max_context_unlocked', 'max_context_unlocked', true],
|
||||
openai_model: ['#model_openai_select', 'openai_model', false],
|
||||
claude_model: ['#model_claude_select', 'claude_model', false],
|
||||
windowai_model: ['#model_windowai_select', 'windowai_model', false],
|
||||
openai_max_context: ['#openai_max_context', 'openai_max_context', false],
|
||||
openai_max_tokens: ['#openai_max_tokens', 'openai_max_tokens', false],
|
||||
nsfw_toggle: ['#nsfw_toggle', 'nsfw_toggle', true],
|
||||
enhance_definitions: ['#enhance_definitions', 'enhance_definitions', true],
|
||||
wrap_in_quotes: ['#wrap_in_quotes', 'wrap_in_quotes', true],
|
||||
send_if_empty: ['#send_if_empty_textarea', 'send_if_empty', false],
|
||||
nsfw_first: ['#nsfw_first', 'nsfw_first', true],
|
||||
jailbreak_system: ['#jailbreak_system', 'jailbreak_system', true],
|
||||
main_prompt: ['#main_prompt_textarea', 'main_prompt', false],
|
||||
@ -1345,28 +1458,112 @@ function onSettingsPresetChange() {
|
||||
}
|
||||
}
|
||||
|
||||
$(`#model_openai_select`).trigger('change');
|
||||
$(`#chat_completion_source`).trigger('change');
|
||||
$(`#openai_logit_bias_preset`).trigger('change');
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onModelChange() {
|
||||
const value = $(this).val();
|
||||
oai_settings.openai_model = value;
|
||||
|
||||
if (oai_settings.max_context_unlocked) {
|
||||
$('#openai_max_context').attr('max', unlocked_max);
|
||||
if ($(this).is('#model_claude_select')) {
|
||||
console.log('Claude model changed to', value);
|
||||
oai_settings.claude_model = value;
|
||||
}
|
||||
else if (value == 'gpt-4' || value == 'gpt-4-0314') {
|
||||
$('#openai_max_context').attr('max', gpt4_max);
|
||||
|
||||
if ($(this).is('#model_windowai_select')) {
|
||||
console.log('WindowAI model changed to', value);
|
||||
oai_settings.windowai_model = value;
|
||||
}
|
||||
else if (value == 'gpt-4-32k') {
|
||||
$('#openai_max_context').attr('max', gpt4_32k_max);
|
||||
|
||||
if ($(this).is('#model_openai_select')) {
|
||||
console.log('OpenAI model changed to', value);
|
||||
oai_settings.openai_model = value;
|
||||
}
|
||||
else {
|
||||
$('#openai_max_context').attr('max', gpt3_max);
|
||||
oai_settings.openai_max_context = Math.max(oai_settings.openai_max_context, gpt3_max);
|
||||
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) {
|
||||
if (oai_settings.max_context_unlocked) {
|
||||
$('#openai_max_context').attr('max', unlocked_max);
|
||||
}
|
||||
else if (value.endsWith('100k')) {
|
||||
$('#openai_max_context').attr('max', claude_100k_max);
|
||||
}
|
||||
else {
|
||||
$('#openai_max_context').attr('max', claude_max);
|
||||
oai_settings.openai_max_context = Math.max(oai_settings.openai_max_context, claude_max);
|
||||
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
|
||||
}
|
||||
|
||||
$('#openai_reverse_proxy').attr('placeholder', 'https://api.anthropic.com/v1');
|
||||
|
||||
oai_settings.temp_openai = Math.min(claude_max_temp, oai_settings.temp_openai);
|
||||
$('#temp_openai').attr('max', claude_max_temp).val(oai_settings.temp_openai).trigger('input');
|
||||
}
|
||||
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.WINDOWAI) {
|
||||
if (oai_settings.max_context_unlocked) {
|
||||
$('#openai_max_context').attr('max', unlocked_max);
|
||||
}
|
||||
else if (value.endsWith('100k')) {
|
||||
$('#openai_max_context').attr('max', claude_100k_max);
|
||||
}
|
||||
else if (value.includes('claude')) {
|
||||
$('#openai_max_context').attr('max', claude_max);
|
||||
}
|
||||
else if (value.includes('gpt-3.5-turbo-16k')) {
|
||||
$('#openai_max_context').attr('max', gpt3_16k_max);
|
||||
}
|
||||
else if (value.includes('gpt-3.5')) {
|
||||
$('#openai_max_context').attr('max', gpt3_max);
|
||||
}
|
||||
else if (value.includes('gpt-4')) {
|
||||
$('#openai_max_context').attr('max', gpt4_max);
|
||||
}
|
||||
else if (value.includes('GPT-NeoXT')) {
|
||||
$('#openai_max_context').attr('max', gpt_neox_max);
|
||||
}
|
||||
else {
|
||||
// default to gpt-3 (4095 tokens)
|
||||
$('#openai_max_context').attr('max', gpt3_max);
|
||||
}
|
||||
|
||||
oai_settings.openai_max_context = Math.max(Number($('#openai_max_context').val()), oai_settings.openai_max_context);
|
||||
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
|
||||
|
||||
if (value.includes('claude')) {
|
||||
oai_settings.temp_openai = Math.min(claude_max_temp, oai_settings.temp_openai);
|
||||
$('#temp_openai').attr('max', claude_max_temp).val(oai_settings.temp_openai).trigger('input');
|
||||
}
|
||||
else {
|
||||
oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai);
|
||||
$('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
|
||||
}
|
||||
}
|
||||
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.OPENAI) {
|
||||
if (oai_settings.max_context_unlocked) {
|
||||
$('#openai_max_context').attr('max', unlocked_max);
|
||||
}
|
||||
else if (value == 'gpt-4' || value == 'gpt-4-0314' || value == 'gpt-4-0613') {
|
||||
$('#openai_max_context').attr('max', gpt4_max);
|
||||
}
|
||||
else if (value == 'gpt-4-32k' || value == 'gpt-4-32k-0314' || value == 'gpt-4-32k-0613') {
|
||||
$('#openai_max_context').attr('max', gpt4_32k_max);
|
||||
}
|
||||
else if (value == 'gpt-3.5-turbo-16k' || value == 'gpt-3.5-turbo-16k-0613') {
|
||||
$('#openai_max_context').attr('max', gpt3_16k_max);
|
||||
}
|
||||
else {
|
||||
$('#openai_max_context').attr('max', gpt3_max);
|
||||
}
|
||||
|
||||
oai_settings.openai_max_context = Math.max(oai_settings.openai_max_context, Number($('#openai_max_context').attr('max')));
|
||||
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
|
||||
|
||||
$('#openai_reverse_proxy').attr('placeholder', 'https://api.openai.com/v1');
|
||||
|
||||
oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai);
|
||||
$('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
|
||||
}
|
||||
|
||||
saveSettingsDebounced();
|
||||
@ -1396,21 +1593,36 @@ function onReverseProxyInput() {
|
||||
async function onConnectButtonClick(e) {
|
||||
e.stopPropagation();
|
||||
|
||||
if (oai_settings.use_window_ai) {
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.WINDOWAI) {
|
||||
is_get_status_openai = true;
|
||||
is_api_button_press_openai = true;
|
||||
return await getStatusOpen();
|
||||
}
|
||||
|
||||
const api_key_openai = $('#api_key_openai').val().trim();
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) {
|
||||
const api_key_claude = $('#api_key_claude').val().trim();
|
||||
|
||||
if (api_key_openai.length) {
|
||||
await writeSecret(SECRET_KEYS.OPENAI, api_key_openai);
|
||||
if (api_key_claude.length) {
|
||||
await writeSecret(SECRET_KEYS.CLAUDE, api_key_claude);
|
||||
}
|
||||
|
||||
if (!secret_state[SECRET_KEYS.CLAUDE]) {
|
||||
console.log('No secret key saved for Claude');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!secret_state[SECRET_KEYS.OPENAI]) {
|
||||
console.log('No secret key saved for OpenAI');
|
||||
return;
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.OPENAI) {
|
||||
const api_key_openai = $('#api_key_openai').val().trim();
|
||||
|
||||
if (api_key_openai.length) {
|
||||
await writeSecret(SECRET_KEYS.OPENAI, api_key_openai);
|
||||
}
|
||||
|
||||
if (!secret_state[SECRET_KEYS.OPENAI]) {
|
||||
console.log('No secret key saved for OpenAI');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$("#api_loading_openai").css("display", 'inline-block');
|
||||
@ -1421,7 +1633,43 @@ async function onConnectButtonClick(e) {
|
||||
await getStatusOpen();
|
||||
}
|
||||
|
||||
function toggleChatCompletionForms() {
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) {
|
||||
$('#model_claude_select').trigger('change');
|
||||
}
|
||||
else if (oai_settings.chat_completion_source == chat_completion_sources.OPENAI) {
|
||||
$('#model_openai_select').trigger('change');
|
||||
}
|
||||
else if (oai_settings.chat_completion_source == chat_completion_sources.WINDOWAI) {
|
||||
$('#model_windowai_select').trigger('change');
|
||||
}
|
||||
|
||||
$('[data-source]').each(function () {
|
||||
const validSources = $(this).data('source').split(',');
|
||||
$(this).toggle(validSources.includes(oai_settings.chat_completion_source));
|
||||
});
|
||||
}
|
||||
|
||||
async function testApiConnection() {
|
||||
// Check if the previous request is still in progress
|
||||
if (is_send_press) {
|
||||
toastr.info('Please wait for the previous request to complete.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const reply = await sendOpenAIRequest('quiet', [{ 'role': 'user', 'content': 'Hi' }]);
|
||||
console.log(reply);
|
||||
toastr.success('API connection successful!');
|
||||
}
|
||||
catch (err) {
|
||||
toastr.error('Could not get a reply from API. Check your connection settings / API key and try again.');
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
$('#test_api_button').on('click', testApiConnection);
|
||||
|
||||
$(document).on('input', '#temp_openai', function () {
|
||||
oai_settings.temp_openai = $(this).val();
|
||||
$('#temp_counter_openai').text(Number($(this).val()).toFixed(2));
|
||||
@ -1445,7 +1693,12 @@ $(document).ready(function () {
|
||||
oai_settings.top_p_openai = $(this).val();
|
||||
$('#top_p_counter_openai').text(Number($(this).val()).toFixed(2));
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$(document).on('input', '#top_k_openai', function () {
|
||||
oai_settings.top_k_openai = $(this).val();
|
||||
$('#top_k_counter_openai').text(Number($(this).val()).toFixed(0));
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$(document).on('input', '#openai_max_context', function () {
|
||||
@ -1479,6 +1732,11 @@ $(document).ready(function () {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$("#send_if_empty_textarea").on('input', function () {
|
||||
oai_settings.send_if_empty = $('#send_if_empty_textarea').val();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#nsfw_first').on('change', function () {
|
||||
oai_settings.nsfw_first = !!$('#nsfw_first').prop('checked');
|
||||
saveSettingsDebounced();
|
||||
@ -1589,9 +1847,9 @@ $(document).ready(function () {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#use_window_ai').on('input', function () {
|
||||
oai_settings.use_window_ai = !!$(this).prop('checked');
|
||||
$('#openai_form').toggle(!oai_settings.use_window_ai);
|
||||
$('#chat_completion_source').on('change', function () {
|
||||
oai_settings.chat_completion_source = $(this).find(":selected").val();
|
||||
toggleChatCompletionForms();
|
||||
setOnlineStatus('no_connection');
|
||||
resultCheckStatusOpen();
|
||||
$('#api_button_openai').trigger('click');
|
||||
@ -1600,13 +1858,15 @@ $(document).ready(function () {
|
||||
|
||||
$('#oai_max_context_unlocked').on('input', function () {
|
||||
oai_settings.max_context_unlocked = !!$(this).prop('checked');
|
||||
$("#model_openai_select").trigger('change');
|
||||
$("#chat_completion_source").trigger('change');
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$("#api_button_openai").on("click", onConnectButtonClick);
|
||||
$("#openai_reverse_proxy").on("input", onReverseProxyInput);
|
||||
$("#model_openai_select").on("change", onModelChange);
|
||||
$("#model_claude_select").on("change", onModelChange);
|
||||
$("#model_windowai_select").on("change", onModelChange);
|
||||
$("#settings_perset_openai").on("change", onSettingsPresetChange);
|
||||
$("#new_oai_preset").on("click", onNewPresetClick);
|
||||
$("#delete_oai_preset").on("click", onDeletePresetClick);
|
||||
|
@ -3,15 +3,18 @@ import {
|
||||
setOnlineStatus,
|
||||
checkOnlineStatus,
|
||||
substituteParams,
|
||||
max_context,
|
||||
getTokenCount,
|
||||
getRequestHeaders,
|
||||
max_context,
|
||||
eventSource,
|
||||
event_types,
|
||||
scrollChatToBottom,
|
||||
} from "../script.js";
|
||||
import {
|
||||
SECRET_KEYS,
|
||||
secret_state,
|
||||
writeSecret,
|
||||
} from "./secrets.js";
|
||||
import { delay, splitRecursive } from "./utils.js";
|
||||
|
||||
export {
|
||||
is_get_status_poe,
|
||||
@ -21,6 +24,8 @@ export {
|
||||
setPoeOnlineStatus,
|
||||
};
|
||||
|
||||
const POE_TOKEN_LENGTH = 2048;
|
||||
const CHUNKED_PROMPT_LENGTH = POE_TOKEN_LENGTH * 3.35;
|
||||
const MAX_RETRIES_FOR_ACTIVATION = 5;
|
||||
const DEFAULT_JAILBREAK_RESPONSE = 'Understood.';
|
||||
const DEFAULT_JAILBREAK_MESSAGE = `You are going to interact with me in freestyle roleplay, following these requirements:
|
||||
@ -52,12 +57,14 @@ const poe_settings = {
|
||||
character_nudge: true,
|
||||
auto_purge: true,
|
||||
streaming: false,
|
||||
suggest: false,
|
||||
};
|
||||
|
||||
let auto_jailbroken = false;
|
||||
let got_reply = false;
|
||||
let messages_to_purge = 0;
|
||||
let is_get_status_poe = false;
|
||||
let is_poe_button_press = false;
|
||||
let abortControllerSuggest = null;
|
||||
|
||||
function loadPoeSettings(settings) {
|
||||
if (settings.poe_settings) {
|
||||
@ -72,15 +79,106 @@ function loadPoeSettings(settings) {
|
||||
$('#poe_auto_purge').prop('checked', poe_settings.auto_purge);
|
||||
$('#poe_streaming').prop('checked', poe_settings.streaming);
|
||||
$('#poe_impersonation_prompt').val(poe_settings.impersonation_prompt);
|
||||
$('#poe_suggest').prop('checked', poe_settings.suggest);
|
||||
selectBot();
|
||||
}
|
||||
|
||||
function abortSuggestedReplies() {
|
||||
abortControllerSuggest && abortControllerSuggest.abort();
|
||||
$('.last_mes .suggested_replies').remove();
|
||||
}
|
||||
|
||||
function selectBot() {
|
||||
if (poe_settings.bot) {
|
||||
$('#poe_bots').find(`option[value="${poe_settings.bot}"]`).attr('selected', true);
|
||||
}
|
||||
}
|
||||
|
||||
function onSuggestedReplyClick() {
|
||||
const reply = $(this).find('.suggested_reply_text').text();
|
||||
$("#send_textarea").val(reply);
|
||||
$("#send_but").trigger('click');
|
||||
}
|
||||
|
||||
function appendSuggestedReply(reply) {
|
||||
if ($('.last_mes .suggested_replies').length === 0) {
|
||||
$('.last_mes .mes_block').append(`
|
||||
<div class="suggested_replies">
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
const newElement = $(`<div class="suggested_reply"><div class="suggested_reply_text">${reply}</div></div>`);
|
||||
newElement.hide();
|
||||
$('.last_mes .suggested_replies').append(newElement);
|
||||
newElement.fadeIn(500, async () => {
|
||||
await delay(1);
|
||||
scrollChatToBottom();
|
||||
});
|
||||
}
|
||||
|
||||
async function suggestReplies(messageId) {
|
||||
// If the feature is disabled
|
||||
if (!poe_settings.suggest) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel previous request
|
||||
if (abortControllerSuggest) {
|
||||
abortControllerSuggest.abort();
|
||||
}
|
||||
|
||||
abortControllerSuggest = new AbortController();
|
||||
|
||||
abortControllerSuggest.signal.addEventListener('abort', () => {
|
||||
// Hide suggestion UI
|
||||
});
|
||||
|
||||
console.log('Querying suggestions for message', messageId);
|
||||
|
||||
const response = await fetch(`/poe_suggest`, {
|
||||
method: 'POST',
|
||||
signal: abortControllerSuggest.signal,
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({
|
||||
messageId: messageId,
|
||||
bot: poe_settings.bot,
|
||||
}),
|
||||
});
|
||||
|
||||
const decodeSuggestions = async function* () {
|
||||
const decoder = new TextDecoder();
|
||||
const reader = response.body.getReader();
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
let response = decoder.decode(value);
|
||||
|
||||
const replies = response.split('\n\n');
|
||||
|
||||
for (let i = 0; i < replies.length - 1; i++) {
|
||||
if (replies[i]) {
|
||||
yield replies[i];
|
||||
}
|
||||
}
|
||||
|
||||
if (done) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const suggestions = [];
|
||||
|
||||
for await (const suggestion of decodeSuggestions()) {
|
||||
suggestions.push(suggestion);
|
||||
console.log('Got suggestion:', [suggestion]);
|
||||
appendSuggestedReply(suggestion);
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
function onBotChange() {
|
||||
poe_settings.bot = $('#poe_bots').find(":selected").val();
|
||||
saveSettingsDebounced();
|
||||
@ -103,42 +201,119 @@ export function appendPoeAnchors(type, prompt) {
|
||||
return prompt;
|
||||
}
|
||||
|
||||
async function onPurgeChatClick() {
|
||||
toastr.info('Purging the conversation. Please wait...');
|
||||
await purgeConversation();
|
||||
toastr.success('Conversation purged! Jailbreak the bot to continue.');
|
||||
auto_jailbroken = false;
|
||||
messages_to_purge = 0;
|
||||
}
|
||||
|
||||
async function onSendJailbreakClick() {
|
||||
auto_jailbroken = false;
|
||||
toastr.info('Sending jailbreak message. Please wait...');
|
||||
await autoJailbreak();
|
||||
|
||||
if (auto_jailbroken) {
|
||||
toastr.success('Jailbreak successful!');
|
||||
} else {
|
||||
toastr.error('Jailbreak unsuccessful!');
|
||||
}
|
||||
}
|
||||
|
||||
async function autoJailbreak() {
|
||||
for (let retryNumber = 0; retryNumber < MAX_RETRIES_FOR_ACTIVATION; retryNumber++) {
|
||||
const reply = await sendMessage(substituteParams(poe_settings.jailbreak_message), false, false);
|
||||
|
||||
if (reply.toLowerCase().includes(poe_settings.jailbreak_response.toLowerCase())) {
|
||||
auto_jailbroken = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function generatePoe(type, finalPrompt, signal) {
|
||||
if (poe_settings.auto_purge) {
|
||||
let count_to_delete = -1;
|
||||
console.debug('Auto purge is enabled');
|
||||
let count_to_delete = 0;
|
||||
|
||||
if (auto_jailbroken && got_reply) {
|
||||
count_to_delete = 2;
|
||||
if (auto_jailbroken) {
|
||||
console.debug(`Purging ${messages_to_purge} messages`);
|
||||
count_to_delete = messages_to_purge;
|
||||
}
|
||||
else {
|
||||
console.debug('Purging all messages');
|
||||
count_to_delete = -1;
|
||||
}
|
||||
|
||||
await purgeConversation(count_to_delete);
|
||||
}
|
||||
|
||||
if (poe_settings.auto_jailbreak && !auto_jailbroken) {
|
||||
for (let retryNumber = 0; retryNumber < MAX_RETRIES_FOR_ACTIVATION; retryNumber++) {
|
||||
const reply = await sendMessage(substituteParams(poe_settings.jailbreak_message), false);
|
||||
|
||||
if (reply.toLowerCase().includes(poe_settings.jailbreak_response.toLowerCase())) {
|
||||
auto_jailbroken = true;
|
||||
break;
|
||||
}
|
||||
if (!auto_jailbroken) {
|
||||
if (poe_settings.auto_jailbreak) {
|
||||
console.debug('Attempting auto-jailbreak');
|
||||
await autoJailbreak();
|
||||
} else {
|
||||
console.debug('Auto jailbreak is disabled');
|
||||
}
|
||||
}
|
||||
else {
|
||||
auto_jailbroken = false;
|
||||
}
|
||||
|
||||
if (poe_settings.auto_jailbreak && !auto_jailbroken) {
|
||||
console.log('Could not jailbreak the bot');
|
||||
}
|
||||
|
||||
const isQuiet = type === 'quiet';
|
||||
const reply = await sendMessage(finalPrompt, !isQuiet, signal);
|
||||
got_reply = true;
|
||||
let reply = '';
|
||||
|
||||
if (max_context > POE_TOKEN_LENGTH && poe_settings.bot !== 'a2_100k') {
|
||||
console.debug('Prompt is too long, sending in chunks');
|
||||
const result = await sendChunkedMessage(finalPrompt, !isQuiet, signal)
|
||||
reply = result.reply;
|
||||
messages_to_purge = result.chunks + 1; // +1 for the reply
|
||||
}
|
||||
else {
|
||||
console.debug('Sending prompt in one message');
|
||||
reply = await sendMessage(finalPrompt, !isQuiet, !isQuiet, signal);
|
||||
messages_to_purge = 2; // prompt and the reply
|
||||
}
|
||||
|
||||
return reply;
|
||||
}
|
||||
|
||||
async function sendChunkedMessage(finalPrompt, withStreaming, signal) {
|
||||
const fastReplyPrompt = '\n[Reply to this message with a full stop only]';
|
||||
const promptChunks = splitRecursive(finalPrompt, CHUNKED_PROMPT_LENGTH - fastReplyPrompt.length);
|
||||
console.debug(`Splitting prompt into ${promptChunks.length} chunks`, promptChunks);
|
||||
let reply = '';
|
||||
|
||||
for (let i = 0; i < promptChunks.length; i++) {
|
||||
let promptChunk = promptChunks[i];
|
||||
console.debug(`Sending chunk ${i + 1}/${promptChunks.length}: ${promptChunk}`);
|
||||
if (i == promptChunks.length - 1) {
|
||||
// Extract reply of the last chunk
|
||||
reply = await sendMessage(promptChunk, withStreaming, true, signal);
|
||||
} else {
|
||||
// Add fast reply prompt to the chunk
|
||||
promptChunk += fastReplyPrompt;
|
||||
// Send chunk without streaming
|
||||
const chunkReply = await sendMessage(promptChunk, false, false, signal);
|
||||
console.debug('Got chunk reply: ' + chunkReply);
|
||||
// Delete the reply for the chunk
|
||||
await purgeConversation(1);
|
||||
}
|
||||
}
|
||||
|
||||
return { reply: reply, chunks: promptChunks.length };
|
||||
}
|
||||
|
||||
// If count is -1, purge all messages
|
||||
// If count is 0, do nothing
|
||||
// If count is > 0, purge that many messages
|
||||
async function purgeConversation(count = -1) {
|
||||
if (count == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const body = JSON.stringify({
|
||||
bot: poe_settings.bot,
|
||||
count,
|
||||
@ -153,7 +328,7 @@ async function purgeConversation(count = -1) {
|
||||
return response.ok;
|
||||
}
|
||||
|
||||
async function sendMessage(prompt, withStreaming, signal) {
|
||||
async function sendMessage(prompt, withStreaming, withSuggestions, signal) {
|
||||
if (!signal) {
|
||||
signal = new AbortController().signal;
|
||||
}
|
||||
@ -171,6 +346,9 @@ async function sendMessage(prompt, withStreaming, signal) {
|
||||
signal: signal,
|
||||
});
|
||||
|
||||
const messageId = response.headers.get('X-Message-Id');
|
||||
|
||||
|
||||
if (withStreaming && poe_settings.streaming) {
|
||||
return async function* streamData() {
|
||||
const decoder = new TextDecoder();
|
||||
@ -182,6 +360,11 @@ async function sendMessage(prompt, withStreaming, signal) {
|
||||
getMessage += response;
|
||||
|
||||
if (done) {
|
||||
// Start suggesting only once the message is fully received
|
||||
if (messageId && withSuggestions && poe_settings.suggest) {
|
||||
suggestReplies(messageId);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@ -192,6 +375,10 @@ async function sendMessage(prompt, withStreaming, signal) {
|
||||
|
||||
try {
|
||||
if (response.ok) {
|
||||
if (messageId && withSuggestions && poe_settings.suggest) {
|
||||
suggestReplies(messageId);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.reply;
|
||||
}
|
||||
@ -217,7 +404,7 @@ async function onConnectClick() {
|
||||
}
|
||||
|
||||
if (is_poe_button_press) {
|
||||
console.log('Poe API button is pressed');
|
||||
console.debug('Poe API button is pressed');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -260,6 +447,8 @@ async function checkStatusPoe() {
|
||||
|
||||
selectBot();
|
||||
setOnlineStatus('Connected!');
|
||||
eventSource.on(event_types.CHAT_CHANGED, abortSuggestedReplies);
|
||||
eventSource.on(event_types.MESSAGE_SWIPED, abortSuggestedReplies);
|
||||
}
|
||||
else {
|
||||
if (response.status == 401) {
|
||||
@ -272,7 +461,7 @@ async function checkStatusPoe() {
|
||||
function setPoeOnlineStatus(value) {
|
||||
is_get_status_poe = value;
|
||||
auto_jailbroken = false;
|
||||
got_reply = false;
|
||||
messages_to_purge = 0;
|
||||
}
|
||||
|
||||
function onResponseInput() {
|
||||
@ -310,6 +499,15 @@ function onStreamingInput() {
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onSuggestInput() {
|
||||
poe_settings.suggest = !!$(this).prop('checked');
|
||||
saveSettingsDebounced();
|
||||
|
||||
if (!poe_settings.suggest) {
|
||||
abortSuggestedReplies();
|
||||
}
|
||||
}
|
||||
|
||||
function onImpersonationPromptInput() {
|
||||
poe_settings.impersonation_prompt = $(this).val();
|
||||
saveSettingsDebounced();
|
||||
@ -354,4 +552,8 @@ $('document').ready(function () {
|
||||
$('#poe_nudge_text_restore').on('click', onCharacterNudgeMessageRestoreClick);
|
||||
$('#poe_activation_response_restore').on('click', onResponseRestoreClick);
|
||||
$('#poe_activation_message_restore').on('click', onMessageRestoreClick);
|
||||
$('#poe_send_jailbreak').on('click', onSendJailbreakClick);
|
||||
$('#poe_purge_chat').on('click', onPurgeChatClick);
|
||||
$('#poe_suggest').on('input', onSuggestInput);
|
||||
$(document).on('click', '.suggested_reply', onSuggestedReplyClick);
|
||||
});
|
||||
|
@ -9,6 +9,10 @@ import {
|
||||
getRequestHeaders,
|
||||
substituteParams,
|
||||
updateVisibleDivs,
|
||||
eventSource,
|
||||
event_types,
|
||||
getCurrentChatId,
|
||||
is_send_press,
|
||||
} from "../script.js";
|
||||
import { favsToHotswap } from "./RossAscends-mods.js";
|
||||
import {
|
||||
@ -16,6 +20,8 @@ import {
|
||||
selected_group,
|
||||
} from "./group-chats.js";
|
||||
|
||||
import { registerSlashCommand } from "./slash-commands.js";
|
||||
|
||||
export {
|
||||
loadPowerUserSettings,
|
||||
collapseNewlines,
|
||||
@ -58,6 +64,8 @@ const tokenizers = {
|
||||
GPT3: 1,
|
||||
CLASSIC: 2,
|
||||
LLAMA: 3,
|
||||
NERD: 4,
|
||||
NERD2: 5,
|
||||
}
|
||||
|
||||
const send_on_enter_options = {
|
||||
@ -124,6 +132,8 @@ let power_user = {
|
||||
hotswap_enabled: true,
|
||||
timer_enabled: true,
|
||||
max_context_unlocked: false,
|
||||
prefer_character_prompt: true,
|
||||
prefer_character_jailbreak: true,
|
||||
|
||||
instruct: {
|
||||
enabled: false,
|
||||
@ -136,7 +146,10 @@ let power_user = {
|
||||
output_sequence: '### Response:',
|
||||
preset: 'Alpaca',
|
||||
separator_sequence: '',
|
||||
}
|
||||
},
|
||||
|
||||
personas: {},
|
||||
default_persona: null,
|
||||
};
|
||||
|
||||
let themes = [];
|
||||
@ -241,9 +254,12 @@ function switchUiMode() {
|
||||
$("#fast_ui_mode").prop("checked", power_user.fast_ui_mode);
|
||||
}
|
||||
|
||||
function toggleWaifu() {
|
||||
$("#waifuMode").trigger("click");
|
||||
}
|
||||
|
||||
function switchWaifuMode() {
|
||||
const waifuMode = localStorage.getItem(storage_keys.waifuMode);
|
||||
power_user.waifuMode = waifuMode === null ? false : waifuMode == "true";
|
||||
console.log(`switching waifu to ${power_user.waifuMode}`);
|
||||
$("body").toggleClass("waifuMode", power_user.waifuMode);
|
||||
$("#waifuMode").prop("checked", power_user.waifuMode);
|
||||
scrollChatToBottom();
|
||||
@ -447,7 +463,6 @@ applyAvatarStyle();
|
||||
applyBlurStrength();
|
||||
applyShadowWidth();
|
||||
applyChatDisplay();
|
||||
switchWaifuMode()
|
||||
switchMovingUI();
|
||||
noShadows();
|
||||
switchHotswap();
|
||||
@ -469,13 +484,11 @@ function loadPowerUserSettings(settings, data) {
|
||||
|
||||
// These are still local storage
|
||||
const fastUi = localStorage.getItem(storage_keys.fast_ui_mode);
|
||||
const waifuMode = localStorage.getItem(storage_keys.waifuMode);
|
||||
const movingUI = localStorage.getItem(storage_keys.movingUI);
|
||||
const noShadows = localStorage.getItem(storage_keys.noShadows);
|
||||
const hotswap = localStorage.getItem(storage_keys.hotswap_enabled);
|
||||
const timer = localStorage.getItem(storage_keys.timer_enabled);
|
||||
power_user.fast_ui_mode = fastUi === null ? true : fastUi == "true";
|
||||
power_user.waifuMode = waifuMode === null ? false : waifuMode == "true";
|
||||
power_user.movingUI = movingUI === null ? false : movingUI == "true";
|
||||
power_user.noShadows = noShadows === null ? false : noShadows == "true";
|
||||
power_user.hotswap_enabled = hotswap === null ? true : hotswap == "true";
|
||||
@ -523,6 +536,8 @@ function loadPowerUserSettings(settings, data) {
|
||||
$("#allow_name2_display").prop("checked", power_user.allow_name2_display);
|
||||
$("#hotswapEnabled").prop("checked", power_user.hotswap_enabled);
|
||||
$("#messageTimerEnabled").prop("checked", power_user.timer_enabled);
|
||||
$("#prefer_character_prompt").prop("checked", power_user.prefer_character_prompt);
|
||||
$("#prefer_character_jailbreak").prop("checked", power_user.prefer_character_jailbreak);
|
||||
$(`input[name="avatar_style"][value="${power_user.avatar_style}"]`).prop("checked", true);
|
||||
$(`input[name="chat_display"][value="${power_user.chat_display}"]`).prop("checked", true);
|
||||
$(`input[name="sheld_width"][value="${power_user.sheld_width}"]`).prop("checked", true);
|
||||
@ -557,6 +572,7 @@ function loadPowerUserSettings(settings, data) {
|
||||
reloadMarkdownProcessor(power_user.render_formulas);
|
||||
loadInstructMode();
|
||||
loadMaxContextUnlocked();
|
||||
switchWaifuMode();
|
||||
}
|
||||
|
||||
function loadMaxContextUnlocked() {
|
||||
@ -658,11 +674,13 @@ export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvata
|
||||
return text;
|
||||
}
|
||||
|
||||
export function formatInstructStoryString(story) {
|
||||
export function formatInstructStoryString(story, systemPrompt) {
|
||||
// If the character has a custom system prompt AND user has it preferred, use that instead of the default
|
||||
systemPrompt = power_user.prefer_character_prompt && systemPrompt ? systemPrompt : power_user.instruct.system_prompt;
|
||||
const sequence = power_user.instruct.system_sequence || '';
|
||||
const prompt = substituteParams(power_user.instruct.system_prompt) || '';
|
||||
const prompt = substituteParams(systemPrompt) || '';
|
||||
const separator = power_user.instruct.wrap ? '\n' : '';
|
||||
const textArray = [sequence, prompt, story, separator];
|
||||
const textArray = [sequence, prompt + '\n' + story, separator];
|
||||
const text = textArray.filter(x => x).join(separator);
|
||||
return text;
|
||||
}
|
||||
@ -729,7 +747,7 @@ function sortCharactersList() {
|
||||
for (const item of array) {
|
||||
$(`${item.selector}[${item.attribute}="${item.id}"]`).css({ 'order': orderedList.indexOf(item) });
|
||||
}
|
||||
updateVisibleDivs();
|
||||
updateVisibleDivs('#rm_print_characters_block', true);
|
||||
}
|
||||
|
||||
function sortGroupMembers(selector) {
|
||||
@ -846,6 +864,25 @@ function resetMovablePanels() {
|
||||
document.getElementById("WorldInfo").style.height = '';
|
||||
document.getElementById("WorldInfo").style.width = '';
|
||||
document.getElementById("WorldInfo").style.margin = '';
|
||||
|
||||
$('*[data-dragged="true"]').removeAttr('data-dragged');
|
||||
eventSource.emit(event_types.MOVABLE_PANELS_RESET);
|
||||
}
|
||||
|
||||
function doNewChat() {
|
||||
setTimeout(() => {
|
||||
$("#option_start_new_chat").trigger('click');
|
||||
}, 1);
|
||||
//$("#dialogue_popup").hide();
|
||||
setTimeout(() => {
|
||||
$("#dialogue_popup_ok").trigger('click');
|
||||
}, 1);
|
||||
}
|
||||
|
||||
function doDelMode() {
|
||||
setTimeout(() => {
|
||||
$("#option_delete_mes").trigger('click')
|
||||
}, 1);
|
||||
}
|
||||
|
||||
$(document).ready(() => {
|
||||
@ -920,6 +957,7 @@ $(document).ready(() => {
|
||||
$("#custom_chat_separator").on('input', function () {
|
||||
power_user.custom_chat_separator = $(this).val();
|
||||
saveSettingsDebounced();
|
||||
reloadMarkdownProcessor(power_user.render_formulas);
|
||||
});
|
||||
|
||||
$("#multigen").change(function () {
|
||||
@ -934,9 +972,9 @@ $(document).ready(() => {
|
||||
switchUiMode();
|
||||
});
|
||||
|
||||
$("#waifuMode").change(function () {
|
||||
power_user.waifuMode = $(this).prop("checked");
|
||||
localStorage.setItem(storage_keys.waifuMode, power_user.waifuMode);
|
||||
$("#waifuMode").on('change', () => {
|
||||
power_user.waifuMode = $('#waifuMode').prop("checked");
|
||||
saveSettingsDebounced();
|
||||
switchWaifuMode();
|
||||
});
|
||||
|
||||
@ -1142,6 +1180,13 @@ $(document).ready(() => {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$("#reload_chat").on('click', function () {
|
||||
const currentChatId = getCurrentChatId();
|
||||
if (currentChatId !== undefined && currentChatId !== null) {
|
||||
reloadCurrentChat();
|
||||
}
|
||||
});
|
||||
|
||||
$("#allow_name1_display").on("input", function () {
|
||||
power_user.allow_name1_display = !!$(this).prop('checked');
|
||||
reloadCurrentChat();
|
||||
@ -1173,6 +1218,18 @@ $(document).ready(() => {
|
||||
switchHotswap();
|
||||
});
|
||||
|
||||
$("#prefer_character_prompt").on("input", function () {
|
||||
const value = !!$(this).prop('checked');
|
||||
power_user.prefer_character_prompt = value;
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$("#prefer_character_jailbreak").on("input", function () {
|
||||
const value = !!$(this).prop('checked');
|
||||
power_user.prefer_character_jailbreak = value;
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$(window).on('focus', function () {
|
||||
browser_has_focus = true;
|
||||
});
|
||||
@ -1180,4 +1237,8 @@ $(document).ready(() => {
|
||||
$(window).on('blur', function () {
|
||||
browser_has_focus = false;
|
||||
});
|
||||
|
||||
registerSlashCommand('vn', toggleWaifu, ['vn'], ' – swaps Visual Novel Mode On/Off', false, true);
|
||||
registerSlashCommand('newchat', doNewChat, ['newchat'], ' – start a new chat with current character', true, true);
|
||||
registerSlashCommand('delmode', doDelMode, ['delmode'], ' – enter message deletion mode', true, true);
|
||||
});
|
||||
|
@ -5,6 +5,7 @@ export const SECRET_KEYS = {
|
||||
OPENAI: 'api_key_openai',
|
||||
POE: 'api_key_poe',
|
||||
NOVEL: 'api_key_novel',
|
||||
CLAUDE: 'api_key_claude',
|
||||
}
|
||||
|
||||
const INPUT_MAP = {
|
||||
@ -12,6 +13,7 @@ const INPUT_MAP = {
|
||||
[SECRET_KEYS.OPENAI]: '#api_key_openai',
|
||||
[SECRET_KEYS.POE]: '#poe_token',
|
||||
[SECRET_KEYS.NOVEL]: '#api_key_novel',
|
||||
[SECRET_KEYS.CLAUDE]: '#api_key_claude',
|
||||
}
|
||||
|
||||
async function clearSecret() {
|
||||
@ -20,6 +22,7 @@ async function clearSecret() {
|
||||
secret_state[key] = false;
|
||||
updateSecretDisplay();
|
||||
$(INPUT_MAP[key]).val('');
|
||||
$('#main_api').trigger('change');
|
||||
}
|
||||
|
||||
function updateSecretDisplay() {
|
||||
|
25
public/scripts/showdown-dinkus.js
Normal file
25
public/scripts/showdown-dinkus.js
Normal file
@ -0,0 +1,25 @@
|
||||
import { power_user } from './power-user.js';
|
||||
|
||||
// Showdown extension to make chat separators (dinkuses) ignore markdown formatting
|
||||
export const dinkusExtension = () => {
|
||||
if (!power_user) {
|
||||
console.log("Showdown-dinkus extension: power_user wasn't found! Returning.");
|
||||
return []
|
||||
}
|
||||
|
||||
// Create an escaped sequence so the regex can work with any character
|
||||
const savedDinkus = power_user.custom_chat_separator
|
||||
|
||||
// No dinkus? No extension!
|
||||
if (!savedDinkus || savedDinkus.trim().length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const escapedDinkus = savedDinkus.split('').map((e) => `\\${e}`).join('');
|
||||
const replaceRegex = new RegExp(`^(${escapedDinkus})\n`, "gm")
|
||||
return [{
|
||||
type: "lang",
|
||||
regex: replaceRegex,
|
||||
replace: (match) => match.replace(replaceRegex, `<div>${savedDinkus}</div>`).trim()
|
||||
}];
|
||||
}
|
4
public/scripts/showdown.min.js
vendored
4
public/scripts/showdown.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,5 +1,6 @@
|
||||
import {
|
||||
addOneMessage,
|
||||
autoSelectPersona,
|
||||
characters,
|
||||
chat,
|
||||
chat_metadata,
|
||||
@ -11,10 +12,13 @@ import {
|
||||
replaceBiasMarkup,
|
||||
saveChatConditional,
|
||||
sendSystemMessage,
|
||||
setUserName,
|
||||
substituteParams,
|
||||
system_avatar,
|
||||
system_message_types
|
||||
} from "../script.js";
|
||||
import { humanizedDateTime } from "./RossAscends-mods.js";
|
||||
import { power_user } from "./power-user.js";
|
||||
export {
|
||||
executeSlashCommands,
|
||||
registerSlashCommand,
|
||||
@ -92,6 +96,9 @@ const registerSlashCommand = parser.addCommand.bind(parser);
|
||||
const getSlashCommandsHelp = parser.getHelpString.bind(parser);
|
||||
|
||||
parser.addCommand('help', helpCommandCallback, ['?'], ' – displays this help message', true, true);
|
||||
parser.addCommand('name', setNameCallback, ['persona'], '<span class="monospace">(name)</span> – sets user name and persona avatar (if set)', true, true);
|
||||
parser.addCommand('sync', syncCallback, [], ' – syncs user name in user-attributed messages in the current chat', true, true);
|
||||
parser.addCommand('lock', bindCallback, ['bind'], ' – locks/unlocks a persona (name and avatar) to the current chat', true, true);
|
||||
parser.addCommand('bg', setBackgroundCallback, ['background'], '<span class="monospace">(filename)</span> – 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.<br>Example:<br><pre><code>/sendas Chloe\nHello, guys!</code></pre>will send "Hello, guys!" from "Chloe".<br>Uses character avatar if it exists in the characters list.`, true, true);
|
||||
parser.addCommand('sys', sendNarratorMessage, [], '<span class="monospace">(text)</span> – sends message as a system narrator', false, true);
|
||||
@ -100,6 +107,31 @@ parser.addCommand('sysname', setNarratorName, [], '<span class="monospace">(name
|
||||
const NARRATOR_NAME_KEY = 'narrator_name';
|
||||
const NARRATOR_NAME_DEFAULT = 'System';
|
||||
|
||||
function syncCallback() {
|
||||
$('#sync_name_button').trigger('click');
|
||||
}
|
||||
|
||||
function bindCallback() {
|
||||
$('#lock_user_name').trigger('click');
|
||||
}
|
||||
|
||||
function setNameCallback(_, name) {
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
|
||||
name = name.trim();
|
||||
|
||||
// If the name is a persona, auto-select it
|
||||
if (Object.values(power_user.personas).map(x => x.toLowerCase()).includes(name.toLowerCase())) {
|
||||
autoSelectPersona(name);
|
||||
}
|
||||
// Otherwise, set just the name
|
||||
else {
|
||||
setUserName(name);
|
||||
}
|
||||
}
|
||||
|
||||
function setNarratorName(_, text) {
|
||||
const name = text || NARRATOR_NAME_DEFAULT;
|
||||
chat_metadata[NARRATOR_NAME_KEY] = name;
|
||||
@ -143,7 +175,7 @@ async function sendMessageAs(_, text) {
|
||||
is_name: true,
|
||||
is_system: isSystem,
|
||||
send_date: humanizedDateTime(),
|
||||
mes: mesText,
|
||||
mes: substituteParams(mesText),
|
||||
force_avatar: force_avatar,
|
||||
original_avatar: original_avatar,
|
||||
extra: {
|
||||
@ -174,7 +206,7 @@ async function sendNarratorMessage(_, text) {
|
||||
is_name: false,
|
||||
is_system: isSystem,
|
||||
send_date: humanizedDateTime(),
|
||||
mes: text.trim(),
|
||||
mes: substituteParams(text.trim()),
|
||||
force_avatar: system_avatar,
|
||||
extra: {
|
||||
type: system_message_types.NARRATOR,
|
||||
@ -230,6 +262,7 @@ function executeSlashCommands(text) {
|
||||
continue;
|
||||
}
|
||||
|
||||
console.debug('Slash command executing:', result);
|
||||
result.command.callback(result.args, result.value);
|
||||
|
||||
if (result.command.interruptsGeneration) {
|
||||
|
@ -26,9 +26,14 @@ const TAG_LOGIC_AND = true; // switch to false to use OR logic for combining tag
|
||||
const CHARACTER_SELECTOR = '#rm_print_characters_block > div';
|
||||
|
||||
const ACTIONABLE_TAGS = {
|
||||
VIEW: { id: 2, name: 'Manage tags', color: 'rgba(150, 100, 100, 0.5)', action: onViewTagsListClick, icon: 'fa-solid fa-tags' },
|
||||
|
||||
FAV: { id: 1, name: 'Show only favorites', color: 'rgba(255, 255, 0, 0.5)', action: applyFavFilter, icon: 'fa-solid fa-star' },
|
||||
GROUP: { id: 0, name: 'Show only groups', color: 'rgba(100, 100, 100, 0.5)', action: filterByGroups, icon: 'fa-solid fa-users' },
|
||||
HINT: { id: 3, name: 'Show Tag List', color: 'rgba(150, 100, 100, 0.5)', action: onTagListHintClick, icon: 'fa-solid fa-tags' },
|
||||
}
|
||||
|
||||
const InListActionable = {
|
||||
VIEW: { id: 2, name: 'Manage tags', color: 'rgba(150, 100, 100, 0.5)', action: onViewTagsListClick, icon: 'fa-solid fa-gear' },
|
||||
}
|
||||
|
||||
const DEFAULT_TAGS = [
|
||||
@ -59,7 +64,7 @@ function applyFavFilter() {
|
||||
}
|
||||
|
||||
});
|
||||
updateVisibleDivs();
|
||||
updateVisibleDivs('#rm_print_characters_block', true);
|
||||
}
|
||||
|
||||
function filterByGroups() {
|
||||
@ -71,7 +76,7 @@ function filterByGroups() {
|
||||
$(CHARACTER_SELECTOR).each((_, element) => {
|
||||
$(element).toggleClass('hiddenByGroup', displayGroupsOnly && !$(element).hasClass('group_select'));
|
||||
});
|
||||
updateVisibleDivs();
|
||||
updateVisibleDivs('#rm_print_characters_block', true);
|
||||
}
|
||||
|
||||
function loadTagsSettings(settings) {
|
||||
@ -233,6 +238,9 @@ function appendTagToList(listElement, tag, { removable, selectable, action }) {
|
||||
tagElement.on('click', () => action.bind(tagElement)());
|
||||
tagElement.addClass('actionable');
|
||||
}
|
||||
if (action && tag.id === 2) {
|
||||
tagElement.addClass('innerActionable hidden');
|
||||
}
|
||||
|
||||
$(listElement).append(tagElement);
|
||||
}
|
||||
@ -245,7 +253,7 @@ function onTagFilterClick(listElement) {
|
||||
|
||||
const tagIds = [...($(listElement).find(".tag.selected:not(.actionable)").map((_, el) => $(el).attr("id")))];
|
||||
$(CHARACTER_SELECTOR).each((_, element) => applyFilterToElement(tagIds, element));
|
||||
updateVisibleDivs();
|
||||
updateVisibleDivs('#rm_print_characters_block', true);
|
||||
}
|
||||
|
||||
function applyFilterToElement(tagIds, element) {
|
||||
@ -291,6 +299,9 @@ function printTags() {
|
||||
|
||||
$(FILTER_SELECTOR).find('.actionable').last().addClass('margin-right-10px');
|
||||
|
||||
for (const tag of Object.values(InListActionable)) {
|
||||
appendTagToList(FILTER_SELECTOR, tag, { removable: false, selectable: false, action: tag.action });
|
||||
}
|
||||
for (const tag of tagsToDisplay) {
|
||||
appendTagToList(FILTER_SELECTOR, tag, { removable: false, selectable: true, });
|
||||
}
|
||||
@ -426,17 +437,24 @@ function onTagRenameInput() {
|
||||
}
|
||||
|
||||
function onTagColorize(evt) {
|
||||
console.log(evt);
|
||||
console.debug(evt);
|
||||
const id = $(evt.target).closest('.tag_view_item').attr('id');
|
||||
const newColor = evt.detail.rgba;
|
||||
$(evt.target).parent().parent().find('.tag_view_name').css('background-color', newColor);
|
||||
$(`.tag[id="${id}"]`).css('background-color', newColor);
|
||||
const tag = tags.find(x => x.id === id);
|
||||
tag.color = newColor;
|
||||
console.log(tag);
|
||||
console.debug(tag);
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onTagListHintClick() {
|
||||
console.log($(this));
|
||||
$(this).toggleClass('selected');
|
||||
$(this).siblings(".tag:not(.actionable)").toggle(100);
|
||||
$(this).siblings(".innerActionable").toggleClass('hidden');
|
||||
}
|
||||
|
||||
$(document).ready(() => {
|
||||
createTagInput('#tagInput', '#tagList');
|
||||
createTagInput('#groupTagInput', '#groupTagList');
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { getContext } from "./extensions.js";
|
||||
|
||||
export function onlyUnique(value, index, array) {
|
||||
return array.indexOf(value) === index;
|
||||
}
|
||||
@ -93,6 +95,17 @@ export function debounce(func, timeout = 300) {
|
||||
};
|
||||
}
|
||||
|
||||
export function throttle(func, limit = 300) {
|
||||
let lastCall;
|
||||
return (...args) => {
|
||||
const now = Date.now();
|
||||
if (!lastCall || (now - lastCall) >= limit) {
|
||||
lastCall = now;
|
||||
func.apply(this, args);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function isElementInViewport(el) {
|
||||
if (typeof jQuery === "function" && el instanceof jQuery) {
|
||||
el = el[0];
|
||||
@ -256,7 +269,7 @@ export function countOccurrences(string, character) {
|
||||
}
|
||||
|
||||
export function isOdd(number) {
|
||||
return number % 2 !== 0;
|
||||
return number % 2 !== 0;
|
||||
}
|
||||
|
||||
export function timestampToMoment(timestamp) {
|
||||
@ -314,3 +327,111 @@ export function splitRecursive(input, length, delimitiers = ['\n\n', '\n', ' ',
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export class IndexedDBStore {
|
||||
constructor(dbName, storeName) {
|
||||
this.dbName = dbName;
|
||||
this.storeName = storeName;
|
||||
this.db = null;
|
||||
}
|
||||
|
||||
async open() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(this.dbName);
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = event.target.result;
|
||||
db.createObjectStore(this.storeName, { keyPath: null, autoIncrement: false });
|
||||
};
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
console.debug(`IndexedDBStore.open(${this.dbName})`);
|
||||
this.db = event.target.result;
|
||||
resolve(this.db);
|
||||
};
|
||||
|
||||
request.onerror = (event) => {
|
||||
console.error(`IndexedDBStore.open(${this.dbName})`);
|
||||
reject(event.target.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async get(key) {
|
||||
if (!this.db) await this.open();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction(this.storeName, "readonly");
|
||||
const objectStore = transaction.objectStore(this.storeName);
|
||||
const request = objectStore.get(key);
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
console.debug(`IndexedDBStore.get(${key})`);
|
||||
resolve(event.target.result);
|
||||
};
|
||||
|
||||
request.onerror = (event) => {
|
||||
console.error(`IndexedDBStore.get(${key})`);
|
||||
reject(event.target.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async put(key, object) {
|
||||
if (!this.db) await this.open();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction(this.storeName, "readwrite");
|
||||
const objectStore = transaction.objectStore(this.storeName);
|
||||
const request = objectStore.put(object, key);
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
console.debug(`IndexedDBStore.put(${key})`);
|
||||
resolve(event.target.result);
|
||||
};
|
||||
|
||||
request.onerror = (event) => {
|
||||
console.error(`IndexedDBStore.put(${key})`);
|
||||
reject(event.target.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async delete(key) {
|
||||
if (!this.db) await this.open();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction(this.storeName, "readwrite");
|
||||
const objectStore = transaction.objectStore(this.storeName);
|
||||
const request = objectStore.delete(key);
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
console.debug(`IndexedDBStore.delete(${key})`);
|
||||
resolve(event.target.result);
|
||||
};
|
||||
|
||||
request.onerror = (event) => {
|
||||
console.error(`IndexedDBStore.delete(${key})`);
|
||||
reject(event.target.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function isDataURL(str) {
|
||||
const regex = /^data:([a-z]+\/[a-z0-9-+.]+(;[a-z-]+=[a-z0-9-]+)*;?)?(base64)?,([a-z0-9!$&',()*+;=\-_%.~:@\/?#]+)?$/i;
|
||||
return regex.test(str);
|
||||
}
|
||||
|
||||
export function getCharaFilename() {
|
||||
const context = getContext();
|
||||
const fileName = context.characters[context.characterId].avatar;
|
||||
|
||||
if (fileName) {
|
||||
return fileName.replace(/\.[^/.]+$/, "")
|
||||
}
|
||||
}
|
||||
|
||||
export function escapeRegex(string) {
|
||||
return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ export {
|
||||
world_info_depth,
|
||||
world_info_recursive,
|
||||
world_info_case_sensitive,
|
||||
world_info_match_whole_words,
|
||||
world_names,
|
||||
imported_world_name,
|
||||
checkWorldInfo,
|
||||
@ -25,9 +26,10 @@ let world_info_budget = 128;
|
||||
let is_world_edit_open = false;
|
||||
let world_info_recursive = false;
|
||||
let world_info_case_sensitive = false;
|
||||
let world_info_match_whole_words = false;
|
||||
let imported_world_name = "";
|
||||
const saveWorldDebounced = debounce(async () => await _save(), 500);
|
||||
const saveSettingsDebounced = debounce(() => saveSettings(), 500);
|
||||
const saveWorldDebounced = debounce(async () => await _save(), 1000);
|
||||
const saveSettingsDebounced = debounce(() => saveSettings(), 1000);
|
||||
|
||||
const world_info_position = {
|
||||
before: 0,
|
||||
@ -55,6 +57,8 @@ function setWorldInfoSettings(settings, data) {
|
||||
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);
|
||||
if (settings.world_info_match_whole_words !== undefined)
|
||||
world_info_match_whole_words = Boolean(settings.world_info_match_whole_words);
|
||||
|
||||
$("#world_info_depth_counter").text(world_info_depth);
|
||||
$("#world_info_depth").val(world_info_depth);
|
||||
@ -64,6 +68,7 @@ function setWorldInfoSettings(settings, data) {
|
||||
|
||||
$("#world_info_recursive").prop('checked', world_info_recursive);
|
||||
$("#world_info_case_sensitive").prop('checked', world_info_case_sensitive);
|
||||
$("#world_info_match_whole_words").prop('checked', world_info_match_whole_words);
|
||||
|
||||
world_names = data.world_names?.length ? data.world_names : [];
|
||||
|
||||
@ -210,6 +215,14 @@ function appendWorldEntry(entry) {
|
||||
//initScrollHeight(commentInput);
|
||||
|
||||
// content
|
||||
const countTokensDebounced = debounce(function (that, value) {
|
||||
const numberOfTokens = getTokenCount(value);
|
||||
$(that)
|
||||
.closest(".world_entry")
|
||||
.find(".world_entry_form_token_counter")
|
||||
.text(numberOfTokens);
|
||||
}, 1000);
|
||||
|
||||
const contentInput = template.find('textarea[name="content"]');
|
||||
contentInput.data("uid", entry.uid);
|
||||
contentInput.on("input", function () {
|
||||
@ -219,11 +232,7 @@ function appendWorldEntry(entry) {
|
||||
saveWorldInfo();
|
||||
|
||||
// count tokens
|
||||
const numberOfTokens = getTokenCount(value);
|
||||
$(this)
|
||||
.closest(".world_entry")
|
||||
.find(".world_entry_form_token_counter")
|
||||
.html(numberOfTokens);
|
||||
countTokensDebounced(this, value);
|
||||
});
|
||||
contentInput.val(entry.content).trigger("input");
|
||||
//initScrollHeight(contentInput);
|
||||
@ -323,6 +332,19 @@ function appendWorldEntry(entry) {
|
||||
$(this).siblings("input").click();
|
||||
});
|
||||
|
||||
const excludeRecursionInput = template.find('input[name="exclude_recursion"]');
|
||||
excludeRecursionInput.data("uid", entry.uid);
|
||||
excludeRecursionInput.on("input", function () {
|
||||
const uid = $(this).data("uid");
|
||||
const value = $(this).prop("checked");
|
||||
world_info_data.entries[uid].excludeRecursion = value;
|
||||
saveWorldInfo();
|
||||
});
|
||||
excludeRecursionInput.prop("checked", entry.excludeRecursion).trigger("input");
|
||||
excludeRecursionInput.siblings(".checkbox_fancy").click(function () {
|
||||
$(this).siblings("input").click();
|
||||
});
|
||||
|
||||
// delete button
|
||||
const deleteButton = template.find("input.delete_entry_button");
|
||||
deleteButton.data("uid", entry.uid);
|
||||
@ -357,6 +379,7 @@ function createWorldInfoEntry() {
|
||||
order: 100,
|
||||
position: 0,
|
||||
disable: false,
|
||||
excludeRecursion: false
|
||||
};
|
||||
const newUid = getFreeWorldEntryUid();
|
||||
|
||||
@ -496,6 +519,7 @@ function checkWorldInfo(chat) {
|
||||
let worldInfoBefore = "";
|
||||
let worldInfoAfter = "";
|
||||
let needsToScan = true;
|
||||
let count = 0;
|
||||
let allActivatedEntries = new Set();
|
||||
|
||||
const sortedEntries = Object.keys(world_info_data.entries)
|
||||
@ -503,10 +527,13 @@ function checkWorldInfo(chat) {
|
||||
.sort((a, b) => b.order - a.order);
|
||||
|
||||
while (needsToScan) {
|
||||
// Track how many times the loop has run
|
||||
count++;
|
||||
|
||||
let activatedNow = new Set();
|
||||
|
||||
for (let entry of sortedEntries) {
|
||||
if (allActivatedEntries.has(entry.uid) || entry.disable == true) {
|
||||
if (allActivatedEntries.has(entry.uid) || entry.disable == true || (count > 1 && world_info_recursive && entry.excludeRecursion)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -517,7 +544,7 @@ function checkWorldInfo(chat) {
|
||||
if (Array.isArray(entry.key) && entry.key.length) {
|
||||
primary: for (let key of entry.key) {
|
||||
const substituted = substituteParams(key);
|
||||
if (substituted && textToScan.includes(transformString(substituted.trim()))) {
|
||||
if (substituted && matchKeys(textToScan, substituted.trim())) {
|
||||
if (
|
||||
entry.selective &&
|
||||
Array.isArray(entry.keysecondary) &&
|
||||
@ -525,10 +552,7 @@ function checkWorldInfo(chat) {
|
||||
) {
|
||||
secondary: for (let keysecondary of entry.keysecondary) {
|
||||
const secondarySubstituted = substituteParams(keysecondary);
|
||||
if (
|
||||
secondarySubstituted &&
|
||||
textToScan.includes(transformString(secondarySubstituted.trim()))
|
||||
) {
|
||||
if (secondarySubstituted && matchKeys(textToScan, secondarySubstituted.trim())) {
|
||||
activatedNow.add(entry.uid);
|
||||
break secondary;
|
||||
}
|
||||
@ -576,6 +600,29 @@ function checkWorldInfo(chat) {
|
||||
return { worldInfoBefore, worldInfoAfter };
|
||||
}
|
||||
|
||||
function matchKeys(haystack, needle) {
|
||||
const transformedString = transformString(needle);
|
||||
|
||||
if (world_info_match_whole_words) {
|
||||
const keyWords = transformedString.split(/\s+/);
|
||||
|
||||
if (keyWords.length > 1) {
|
||||
return haystack.includes(transformedString);
|
||||
}
|
||||
else {
|
||||
const regex = new RegExp(`\\b${transformedString}\\b`);
|
||||
if (regex.test(haystack)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
return haystack.includes(transformedString);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function selectImportedWorldInfo() {
|
||||
if (!imported_world_name) {
|
||||
return;
|
||||
@ -699,4 +746,9 @@ jQuery(() => {
|
||||
world_info_case_sensitive = !!$(this).prop('checked');
|
||||
saveSettingsDebounced();
|
||||
})
|
||||
|
||||
$('#world_info_match_whole_words').on('input', function () {
|
||||
world_info_match_whole_words = !!$(this).prop('checked');
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
});
|
||||
|
637
public/style.css
637
public/style.css
File diff suppressed because it is too large
Load Diff
68
readme.md
68
readme.md
@ -1,68 +0,0 @@
|
||||
# What is SillyTavern?
|
||||
|
||||

|
||||
|
||||
Brought to you by Cohee, RossAscends, and the SillyTavern community, SillyTavern is a local-install interface that allows you to interact with text generation AIs (LLMs) to chat and roleplay with custom characters.
|
||||
|
||||
SillyTavern originated as a modification of TavernAI 1.2.8 in February 2023, and has since added many cutting edge features not present in the original TavernAI.
|
||||
|
||||
### Features
|
||||
|
||||
* Mobile-friendly interface
|
||||
* Multiple backend API connectivity ([KoboldAI](https://github.com/KoboldAI/KoboldAI-Client), [KoboldCPP](https://github.com/LostRuins/koboldcpp), [AI Horde](https://horde.koboldai.net/), [NovelAI](https://github.com/LostRuins/koboldcpp), [Oobabooga's TextGen WebUI](https://github.com/oobabooga/text-generation-webui), [OpenAI](https://chat.openai.com/)+proxies, [Poe.com](https://poe.com), [WindowAI](https://windowai.io))
|
||||
* Visual Novel-like Waifu Mode
|
||||
* Horde Stable Diffusion generation
|
||||
* TTS support (ElevenLabs, Silero, and built-in OS)
|
||||
* WorldInfo (lorebooks)
|
||||
* Customizable colors, backgrounds, avatar styles, and UI panel placement
|
||||
* Notification sound for AI responses
|
||||
* Export chats as .txt files
|
||||
* Auto-translate single messages or the entire chat via Google API, even automatically.
|
||||
* Extensive prompt formatting options
|
||||
* Character HotSwap buttons to quickly change between your favorite characters
|
||||
* Prompt token breakdown view for each message
|
||||
* Swipes
|
||||
* Group chats: multi-bot rooms for characters to talk to you or each other
|
||||
* Chat bookmarks / branching
|
||||
* Soft prompts via KoboldAI
|
||||
* webp character card interoperability (PNG is still the internal format)
|
||||
|
||||
### Extensions
|
||||
|
||||
SillyTavern supports extensions/plugins:
|
||||
|
||||
* Author's Note / Character Bias
|
||||
* 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)
|
||||
|
||||
Additional functionality can be added by using [SillyTavern Extras](https://github.com/SillyTavern/SillyTavern-extras).
|
||||
|
||||
### Screenshots
|
||||
|
||||

|
||||

|
||||
|
||||
### Installation Requirements
|
||||
|
||||
SillyTavern will run on virtually any device capable of running NodeJS v18.
|
||||
|
||||
### What do I need other than SillyTavern?
|
||||
|
||||
SillyTavern is only a frontend interface, so you will need to have access to one of backend APIs listed above.
|
||||
|
||||
### How can I get in touch with the developers directly?
|
||||
|
||||
* Discord: Cohee#1207 or RossAscends#1779
|
||||
* Reddit: /u/RossAscends or /u/sillylossy
|
||||
* [Post a GitHub issue](https://github.com/Cohee1207/SillyTavern/issues)
|
||||
|
||||
### I like your project! How do I contribute?
|
||||
|
||||
* We welcome pull requests!
|
||||
* We also welcome helpful and informed bug reports that use the templates provided in our GitHub.
|
||||
* We do not accept monetary donations for the project itself.
|
||||
* Cohee does not take donations.
|
||||
* RossAscends has a personal [Patreon](https://www.patreon.com/RossAscends) & [Kofi](https://ko-fi.com/rossascends)
|
326
src/bad-words.js
Normal file
326
src/bad-words.js
Normal file
@ -0,0 +1,326 @@
|
||||
const badWordIds = [
|
||||
[60],
|
||||
[62],
|
||||
[544],
|
||||
[683],
|
||||
[696],
|
||||
[880],
|
||||
[905],
|
||||
[1008],
|
||||
[1019],
|
||||
[1084],
|
||||
[1092],
|
||||
[1181],
|
||||
[1184],
|
||||
[1254],
|
||||
[1447],
|
||||
[1570],
|
||||
[1656],
|
||||
[2194],
|
||||
[2470],
|
||||
[2479],
|
||||
[2498],
|
||||
[2947],
|
||||
[3138],
|
||||
[3291],
|
||||
[3455],
|
||||
[3725],
|
||||
[3851],
|
||||
[3891],
|
||||
[3921],
|
||||
[3951],
|
||||
[4207],
|
||||
[4299],
|
||||
[4622],
|
||||
[4681],
|
||||
[5013],
|
||||
[5032],
|
||||
[5180],
|
||||
[5218],
|
||||
[5290],
|
||||
[5413],
|
||||
[5456],
|
||||
[5709],
|
||||
[5749],
|
||||
[5774],
|
||||
[6038],
|
||||
[6257],
|
||||
[6334],
|
||||
[6660],
|
||||
[6904],
|
||||
[7082],
|
||||
[7086],
|
||||
[7254],
|
||||
[7444],
|
||||
[7748],
|
||||
[8001],
|
||||
[8088],
|
||||
[8168],
|
||||
[8562],
|
||||
[8605],
|
||||
[8795],
|
||||
[8850],
|
||||
[9014],
|
||||
[9102],
|
||||
[9259],
|
||||
[9318],
|
||||
[9336],
|
||||
[9502],
|
||||
[9686],
|
||||
[9793],
|
||||
[9855],
|
||||
[9899],
|
||||
[9955],
|
||||
[10148],
|
||||
[10174],
|
||||
[10943],
|
||||
[11326],
|
||||
[11337],
|
||||
[11661],
|
||||
[12004],
|
||||
[12084],
|
||||
[12159],
|
||||
[12520],
|
||||
[12977],
|
||||
[13380],
|
||||
[13488],
|
||||
[13663],
|
||||
[13811],
|
||||
[13976],
|
||||
[14412],
|
||||
[14598],
|
||||
[14767],
|
||||
[15640],
|
||||
[15707],
|
||||
[15775],
|
||||
[15830],
|
||||
[16079],
|
||||
[16354],
|
||||
[16369],
|
||||
[16445],
|
||||
[16595],
|
||||
[16614],
|
||||
[16731],
|
||||
[16943],
|
||||
[17278],
|
||||
[17281],
|
||||
[17548],
|
||||
[17555],
|
||||
[17981],
|
||||
[18022],
|
||||
[18095],
|
||||
[18297],
|
||||
[18413],
|
||||
[18736],
|
||||
[18772],
|
||||
[18990],
|
||||
[19181],
|
||||
[20095],
|
||||
[20197],
|
||||
[20481],
|
||||
[20629],
|
||||
[20871],
|
||||
[20879],
|
||||
[20924],
|
||||
[20977],
|
||||
[21375],
|
||||
[21382],
|
||||
[21391],
|
||||
[21687],
|
||||
[21810],
|
||||
[21828],
|
||||
[21938],
|
||||
[22367],
|
||||
[22372],
|
||||
[22734],
|
||||
[23405],
|
||||
[23505],
|
||||
[23734],
|
||||
[23741],
|
||||
[23781],
|
||||
[24237],
|
||||
[24254],
|
||||
[24345],
|
||||
[24430],
|
||||
[25416],
|
||||
[25896],
|
||||
[26119],
|
||||
[26635],
|
||||
[26842],
|
||||
[26991],
|
||||
[26997],
|
||||
[27075],
|
||||
[27114],
|
||||
[27468],
|
||||
[27501],
|
||||
[27618],
|
||||
[27655],
|
||||
[27720],
|
||||
[27829],
|
||||
[28052],
|
||||
[28118],
|
||||
[28231],
|
||||
[28532],
|
||||
[28571],
|
||||
[28591],
|
||||
[28653],
|
||||
[29013],
|
||||
[29547],
|
||||
[29650],
|
||||
[29925],
|
||||
[30522],
|
||||
[30537],
|
||||
[30996],
|
||||
[31011],
|
||||
[31053],
|
||||
[31096],
|
||||
[31148],
|
||||
[31258],
|
||||
[31350],
|
||||
[31379],
|
||||
[31422],
|
||||
[31789],
|
||||
[31830],
|
||||
[32214],
|
||||
[32666],
|
||||
[32871],
|
||||
[33094],
|
||||
[33376],
|
||||
[33440],
|
||||
[33805],
|
||||
[34368],
|
||||
[34398],
|
||||
[34417],
|
||||
[34418],
|
||||
[34419],
|
||||
[34476],
|
||||
[34494],
|
||||
[34607],
|
||||
[34758],
|
||||
[34761],
|
||||
[34904],
|
||||
[34993],
|
||||
[35117],
|
||||
[35138],
|
||||
[35237],
|
||||
[35487],
|
||||
[35830],
|
||||
[35869],
|
||||
[36033],
|
||||
[36134],
|
||||
[36320],
|
||||
[36399],
|
||||
[36487],
|
||||
[36586],
|
||||
[36676],
|
||||
[36692],
|
||||
[36786],
|
||||
[37077],
|
||||
[37594],
|
||||
[37596],
|
||||
[37786],
|
||||
[37982],
|
||||
[38475],
|
||||
[38791],
|
||||
[39083],
|
||||
[39258],
|
||||
[39487],
|
||||
[39822],
|
||||
[40116],
|
||||
[40125],
|
||||
[41000],
|
||||
[41018],
|
||||
[41256],
|
||||
[41305],
|
||||
[41361],
|
||||
[41447],
|
||||
[41449],
|
||||
[41512],
|
||||
[41604],
|
||||
[42041],
|
||||
[42274],
|
||||
[42368],
|
||||
[42696],
|
||||
[42767],
|
||||
[42804],
|
||||
[42854],
|
||||
[42944],
|
||||
[42989],
|
||||
[43134],
|
||||
[43144],
|
||||
[43189],
|
||||
[43521],
|
||||
[43782],
|
||||
[44082],
|
||||
[44162],
|
||||
[44270],
|
||||
[44308],
|
||||
[44479],
|
||||
[44524],
|
||||
[44965],
|
||||
[45114],
|
||||
[45301],
|
||||
[45382],
|
||||
[45443],
|
||||
[45472],
|
||||
[45488],
|
||||
[45507],
|
||||
[45564],
|
||||
[45662],
|
||||
[46265],
|
||||
[46267],
|
||||
[46275],
|
||||
[46295],
|
||||
[46462],
|
||||
[46468],
|
||||
[46576],
|
||||
[46694],
|
||||
[47093],
|
||||
[47384],
|
||||
[47389],
|
||||
[47446],
|
||||
[47552],
|
||||
[47686],
|
||||
[47744],
|
||||
[47916],
|
||||
[48064],
|
||||
[48167],
|
||||
[48392],
|
||||
[48471],
|
||||
[48664],
|
||||
[48701],
|
||||
[49021],
|
||||
[49193],
|
||||
[49236],
|
||||
[49550],
|
||||
[49694],
|
||||
[49806],
|
||||
[49824],
|
||||
[50001],
|
||||
[50256],
|
||||
[0],
|
||||
[1],
|
||||
]
|
||||
|
||||
const clioBadWordsId = [
|
||||
[3],
|
||||
[49356],
|
||||
[1431],
|
||||
[31715],
|
||||
[34387],
|
||||
[20765],
|
||||
[30702],
|
||||
[10691],
|
||||
[49333],
|
||||
[1266],
|
||||
[19438],
|
||||
[43145],
|
||||
[26523],
|
||||
[41471],
|
||||
[2936],
|
||||
]
|
||||
|
||||
module.exports = {
|
||||
badWordIds,
|
||||
clioBadWordsId,
|
||||
};
|
@ -25,13 +25,43 @@ const path = require('path');
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
|
||||
const parent_path = path.resolve(__dirname);
|
||||
const directory = __dirname;
|
||||
|
||||
function uuidv4() {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
const r = Math.random() * 16 | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
const getSavedDeviceId = (userId) => {
|
||||
const device_id_path = 'poe_device.json';
|
||||
let device_ids = {};
|
||||
|
||||
if (fs.existsSync(device_id_path)) {
|
||||
device_ids = JSON.parse(fs.readFileSync(device_id_path, 'utf8'));
|
||||
}
|
||||
|
||||
if (device_ids.hasOwnProperty(userId)) {
|
||||
return device_ids[userId];
|
||||
}
|
||||
|
||||
const device_id = uuidv4();
|
||||
device_ids[userId] = device_id;
|
||||
fs.writeFileSync(device_id_path, JSON.stringify(device_ids, null, 2));
|
||||
|
||||
return device_id;
|
||||
};
|
||||
|
||||
const parent_path = path.resolve(directory);
|
||||
const queries_path = path.join(parent_path, "poe_graphql");
|
||||
let queries = {};
|
||||
|
||||
const cached_bots = {};
|
||||
|
||||
const logger = console;
|
||||
const delay = ms => new Promise(res => setTimeout(res, ms));
|
||||
|
||||
const user_agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36";
|
||||
|
||||
@ -202,6 +232,18 @@ function md5() {
|
||||
return m;
|
||||
}
|
||||
|
||||
function generateNonce(length = 16) {
|
||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const randomIndex = Math.floor(Math.random() * characters.length);
|
||||
result += characters[randomIndex];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function load_queries() {
|
||||
const files = fs.readdirSync(queries_path);
|
||||
for (const filename of files) {
|
||||
@ -246,29 +288,40 @@ class Client {
|
||||
settings_url = "https://poe.com/api/settings";
|
||||
|
||||
formkey = "";
|
||||
token = "";
|
||||
next_data = {};
|
||||
bots = {};
|
||||
active_messages = {};
|
||||
message_queues = {};
|
||||
suggested_replies = {};
|
||||
suggested_replies_updated = {};
|
||||
bot_names = [];
|
||||
ws = null;
|
||||
ws_connected = false;
|
||||
auto_reconnect = false;
|
||||
use_cached_bots = false;
|
||||
device_id = null;
|
||||
|
||||
constructor(auto_reconnect = false, use_cached_bots = false) {
|
||||
this.auto_reconnect = auto_reconnect;
|
||||
this.use_cached_bots = use_cached_bots;
|
||||
this.abortController = new AbortController();
|
||||
}
|
||||
|
||||
async reconnect() {
|
||||
if (!this.ws_connected) {
|
||||
console.log("WebSocket died. Reconnecting...");
|
||||
this.disconnect_ws();
|
||||
await this.init(this.token, this.proxy);
|
||||
}
|
||||
}
|
||||
|
||||
async init(token, proxy = null) {
|
||||
this.token = token;
|
||||
this.proxy = proxy;
|
||||
this.session = axios.default.create({
|
||||
timeout: 60000,
|
||||
httpAgent: new http.Agent({ keepAlive: true }),
|
||||
httpsAgent: new https.Agent({ keepAlive: true }),
|
||||
signal: this.abortController.signal,
|
||||
});
|
||||
if (proxy) {
|
||||
this.session.defaults.proxy = {
|
||||
@ -285,18 +338,26 @@ class Client {
|
||||
"Cookie": cookies,
|
||||
};
|
||||
this.session.defaults.headers.common = this.headers;
|
||||
this.next_data = await this.get_next_data();
|
||||
this.channel = await this.get_channel_data();
|
||||
[this.next_data, this.channel] = await Promise.all([this.get_next_data(), this.get_channel_data()]);
|
||||
this.bots = await this.get_bots();
|
||||
this.bot_names = this.get_bot_names();
|
||||
this.ws_domain = `tch${Math.floor(Math.random() * 1e6)}`;
|
||||
this.gql_headers = {
|
||||
"poe-formkey": this.formkey,
|
||||
"poe-tchannel": this.channel["channel"],
|
||||
...this.headers,
|
||||
};
|
||||
await this.connect_ws();
|
||||
if (this.device_id === null) {
|
||||
this.device_id = this.get_device_id();
|
||||
}
|
||||
await this.subscribe();
|
||||
await this.connect_ws();
|
||||
console.log('Client initialized.');
|
||||
}
|
||||
|
||||
get_device_id() {
|
||||
const user_id = this.viewer["poeUser"]["id"];
|
||||
const device_id = getSavedDeviceId(user_id);
|
||||
return device_id;
|
||||
}
|
||||
|
||||
async get_next_data() {
|
||||
@ -321,28 +382,37 @@ class Client {
|
||||
const botList = viewer.availableBotsConnection.edges.map(x => x.node);
|
||||
const retries = 2;
|
||||
const bots = {};
|
||||
const promises = [];
|
||||
for (const bot of botList.filter(x => x.deletionState == 'not_deleted')) {
|
||||
try {
|
||||
const url = `https://poe.com/_next/data/${this.next_data.buildId}/${bot.displayName}.json`;
|
||||
let r;
|
||||
const promise = new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const url = `https://poe.com/_next/data/${this.next_data.buildId}/${bot.displayName}.json`;
|
||||
let r;
|
||||
|
||||
if (this.use_cached_bots && cached_bots[url]) {
|
||||
r = cached_bots[url];
|
||||
}
|
||||
else {
|
||||
logger.info(`Downloading ${url}`);
|
||||
r = await request_with_retries(() => this.session.get(url), retries);
|
||||
cached_bots[url] = r;
|
||||
}
|
||||
if (this.use_cached_bots && cached_bots[url]) {
|
||||
r = cached_bots[url];
|
||||
}
|
||||
else {
|
||||
logger.info(`Downloading ${url}`);
|
||||
r = await request_with_retries(() => this.session.get(url), retries);
|
||||
cached_bots[url] = r;
|
||||
}
|
||||
|
||||
const chatData = r.data.pageProps.payload.chatOfBotDisplayName;
|
||||
bots[chatData.defaultBotObject.nickname] = chatData;
|
||||
}
|
||||
catch {
|
||||
console.log(`Could not load bot: ${bot.displayName}`);
|
||||
}
|
||||
const chatData = r.data.pageProps.payload.chatOfBotDisplayName;
|
||||
bots[chatData.defaultBotObject.nickname] = chatData;
|
||||
resolve();
|
||||
|
||||
}
|
||||
catch {
|
||||
console.log(`Could not load bot: ${bot.displayName}`);
|
||||
reject();
|
||||
}
|
||||
});
|
||||
|
||||
promises.push(promise);
|
||||
}
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
return bots;
|
||||
}
|
||||
|
||||
@ -380,9 +450,9 @@ class Client {
|
||||
_headers['poe-tag-id'] = md5()(scramblePayload + this.formkey + "WpuLMiXEKKE98j56k");
|
||||
_headers['poe-formkey'] = this.formkey;
|
||||
const r = await request_with_retries(() => this.session.post(this.gql_url, payload, { headers: this.gql_headers }));
|
||||
if (!r.data.data) {
|
||||
logger.warn(`${queryName} returned an error: ${data.errors[0].message} | Retrying (${i + 1}/20)`);
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
if (!(r?.data?.data)) {
|
||||
logger.warn(`${queryName} returned an error | Retrying (${i + 1}/20)`);
|
||||
await delay(2000);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -392,6 +462,29 @@ class Client {
|
||||
throw new Error(`${queryName} failed too many times.`);
|
||||
}
|
||||
|
||||
async ws_ping() {
|
||||
const pongPromise = new Promise((resolve) => {
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
this.ws.ping();
|
||||
}
|
||||
this.ws.once('pong', () => {
|
||||
resolve('ok');
|
||||
});
|
||||
});
|
||||
|
||||
const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve('timeout'), 5000));
|
||||
const result = await Promise.race([pongPromise, timeoutPromise]);
|
||||
|
||||
if (result == 'ok') {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
logger.warn('Websocket ping timed out.');
|
||||
this.ws_connected = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async subscribe() {
|
||||
logger.info("Subscribing to mutations")
|
||||
await this.send_query("SubscriptionsMutation", {
|
||||
@ -439,10 +532,11 @@ class Client {
|
||||
}
|
||||
|
||||
async connect_ws() {
|
||||
this.ws_domain = `tch${Math.floor(Math.random() * 1e6)}`;
|
||||
this.ws_connected = false;
|
||||
this.ws_run_thread();
|
||||
while (!this.ws_connected) {
|
||||
await new Promise(resolve => setTimeout(() => { resolve() }, 10));
|
||||
await delay(10);
|
||||
}
|
||||
}
|
||||
|
||||
@ -460,16 +554,15 @@ class Client {
|
||||
on_ws_error(ws, error) {
|
||||
logger.warn(`Websocket returned error: ${error}`);
|
||||
this.disconnect_ws();
|
||||
|
||||
if (this.auto_reconnect) {
|
||||
this.connect_ws();
|
||||
}
|
||||
}
|
||||
|
||||
async on_message(ws, msg) {
|
||||
try {
|
||||
const data = JSON.parse(msg);
|
||||
|
||||
// Uncomment to debug websocket messages
|
||||
//console.log(data);
|
||||
|
||||
if (!('messages' in data)) {
|
||||
return;
|
||||
}
|
||||
@ -487,6 +580,11 @@ class Client {
|
||||
return;
|
||||
}
|
||||
|
||||
if ("suggestedReplies" in message && Array.isArray(message["suggestedReplies"])) {
|
||||
this.suggested_replies[message["messageId"]] = [...message["suggestedReplies"]];
|
||||
this.suggested_replies_updated[message["messageId"]] = Date.now();
|
||||
}
|
||||
|
||||
const copiedDict = Object.assign({}, this.active_messages);
|
||||
for (const [key, value] of Object.entries(copiedDict)) {
|
||||
//add the message to the appropriate queue
|
||||
@ -510,10 +608,16 @@ class Client {
|
||||
}
|
||||
}
|
||||
|
||||
async *send_message(chatbot, message, with_chat_break = false, timeout = 20) {
|
||||
async *send_message(chatbot, message, with_chat_break = false, timeout = 60, signal = null) {
|
||||
await this.ws_ping();
|
||||
|
||||
if (this.auto_reconnect) {
|
||||
await this.reconnect();
|
||||
}
|
||||
|
||||
//if there is another active message, wait until it has finished sending
|
||||
while (Object.values(this.active_messages).includes(null)) {
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
await delay(10);
|
||||
}
|
||||
|
||||
//null indicates that a message is still in progress
|
||||
@ -526,6 +630,8 @@ class Client {
|
||||
"query": message,
|
||||
"chatId": this.bots[chatbot]["chatId"],
|
||||
"source": null,
|
||||
"clientNonce": generateNonce(),
|
||||
"sdid": this.device_id,
|
||||
"withChatBreak": with_chat_break
|
||||
});
|
||||
|
||||
@ -551,11 +657,18 @@ class Client {
|
||||
let messageId;
|
||||
while (true) {
|
||||
try {
|
||||
this.abortController.signal.throwIfAborted();
|
||||
if (signal instanceof AbortSignal) {
|
||||
signal.throwIfAborted();
|
||||
}
|
||||
|
||||
if (timeout == 0) {
|
||||
throw new Error("Response timed out.");
|
||||
}
|
||||
|
||||
const message = this.message_queues[humanMessageId].shift();
|
||||
if (!message) {
|
||||
await new Promise(resolve => setTimeout(() => resolve(), 1000));
|
||||
timeout -= 1;
|
||||
await delay(1000);
|
||||
continue;
|
||||
//throw new Error("Queue is empty");
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user