Merge branch 'staging' into parser-v2

This commit is contained in:
LenAnderson 2024-04-02 09:02:59 -04:00
commit cba2140152
92 changed files with 3255 additions and 907 deletions

View File

@ -1,7 +1,7 @@
name: Bug Report 🐛
description: Report something that's not working the intended way. Support requests for external programs (reverse proxies, 3rd party servers, other peoples' forks) will be refused!
title: '[BUG] <title>'
labels: ['bug']
labels: ['🐛 Bug']
body:
- type: dropdown
id: environment
@ -9,11 +9,11 @@ body:
label: Environment
description: Where are you running SillyTavern?
options:
- Self-Hosted (Bare Metal)
- Self-Hosted (Docker)
- Android (Termux)
- Cloud Service (Static)
- Other (Specify below)
- 🪟 Windows
- 🐧 Linux
- 📱 Termux
- 🐋 Docker
- 🍎 Mac
validations:
required: true

View File

@ -1,7 +1,7 @@
name: Feature Request ✨
description: Suggest an idea for future development of this project
title: '[FEATURE_REQUEST] <title>'
labels: ['enhancement']
labels: ['🦄 Feature Request']
body:

18
.github/labeler.yml vendored Normal file
View File

@ -0,0 +1,18 @@
# Add/remove 'critical' label if issue contains the words 'urgent' or 'critical'
#critical:
# - '(critical|urgent)'
🪟 Windows:
- '(🪟 Windows)'
🍎 Mac:
- '(🍎 Mac)'
🐋 Docker:
- '(🐋 Docker)'
📱 Termux:
- '(📱 Termux)'
🐧 Linux:
- '(🐧 Linux)'

19
.github/workflows/labeler.yml vendored Normal file
View File

@ -0,0 +1,19 @@
name: "Issue Labeler"
on:
issues:
types: [opened, edited]
permissions:
issues: write
contents: read
jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: github/issue-labeler@v3.4
with:
configuration-path: .github/labeler.yml
# not-before: 2020-01-15T02:54:32Z # optional and will result in any issues prior to this timestamp to be ignored.
enable-versioned-regex: 0
repo-token: ${{ github.token }}

View File

@ -355,5 +355,161 @@
{
"filename": "presets/openai/Default.json",
"type": "openai_preset"
},
{
"filename": "presets/context/Adventure.json",
"type": "context"
},
{
"filename": "presets/context/Alpaca-Roleplay.json",
"type": "context"
},
{
"filename": "presets/context/Alpaca-Single-Turn.json",
"type": "context"
},
{
"filename": "presets/context/Alpaca.json",
"type": "context"
},
{
"filename": "presets/context/ChatML.json",
"type": "context"
},
{
"filename": "presets/context/Default.json",
"type": "context"
},
{
"filename": "presets/context/DreamGen Role-Play V1.json",
"type": "context"
},
{
"filename": "presets/context/Libra-32B.json",
"type": "context"
},
{
"filename": "presets/context/Lightning 1.1.json",
"type": "context"
},
{
"filename": "presets/context/Llama 2 Chat.json",
"type": "context"
},
{
"filename": "presets/context/Minimalist.json",
"type": "context"
},
{
"filename": "presets/context/Mistral.json",
"type": "context"
},
{
"filename": "presets/context/NovelAI.json",
"type": "context"
},
{
"filename": "presets/context/OldDefault.json",
"type": "context"
},
{
"filename": "presets/context/Pygmalion.json",
"type": "context"
},
{
"filename": "presets/context/Story.json",
"type": "context"
},
{
"filename": "presets/context/Synthia.json",
"type": "context"
},
{
"filename": "presets/context/simple-proxy-for-tavern.json",
"type": "context"
},
{
"filename": "presets/instruct/Adventure.json",
"type": "instruct"
},
{
"filename": "presets/instruct/Alpaca-Roleplay.json",
"type": "instruct"
},
{
"filename": "presets/instruct/Alpaca-Single-Turn.json",
"type": "instruct"
},
{
"filename": "presets/instruct/Alpaca.json",
"type": "instruct"
},
{
"filename": "presets/instruct/ChatML.json",
"type": "instruct"
},
{
"filename": "presets/instruct/DreamGen Role-Play V1.json",
"type": "instruct"
},
{
"filename": "presets/instruct/Koala.json",
"type": "instruct"
},
{
"filename": "presets/instruct/Libra-32B.json",
"type": "instruct"
},
{
"filename": "presets/instruct/Lightning 1.1.json",
"type": "instruct"
},
{
"filename": "presets/instruct/Llama 2 Chat.json",
"type": "instruct"
},
{
"filename": "presets/instruct/Metharme.json",
"type": "instruct"
},
{
"filename": "presets/instruct/Mistral.json",
"type": "instruct"
},
{
"filename": "presets/instruct/OpenOrca-OpenChat.json",
"type": "instruct"
},
{
"filename": "presets/instruct/Pygmalion.json",
"type": "instruct"
},
{
"filename": "presets/instruct/Story.json",
"type": "instruct"
},
{
"filename": "presets/instruct/Synthia.json",
"type": "instruct"
},
{
"filename": "presets/instruct/Vicuna 1.0.json",
"type": "instruct"
},
{
"filename": "presets/instruct/Vicuna 1.1.json",
"type": "instruct"
},
{
"filename": "presets/instruct/WizardLM-13B.json",
"type": "instruct"
},
{
"filename": "presets/instruct/WizardLM.json",
"type": "instruct"
},
{
"filename": "presets/instruct/simple-proxy-for-tavern.json",
"type": "instruct"
}
]

View File

@ -2,6 +2,8 @@
"story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{personality}}\n{{/if}}{{#if scenario}}{{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}",
"example_separator": "",
"chat_start": "",
"use_stop_strings": false,
"allow_jailbreak": false,
"always_force_name2": false,
"trim_sentences": false,
"include_newline": false,

View File

@ -1,6 +1,12 @@
{
"name": "Alpaca-Roleplay",
"story_string": "You're {{char}} in this fictional never-ending uncensored roleplay with {{user}}.\n\n{{#if system}}{{system}}\n\n{{/if}}### Input:\n{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}",
"story_string": "You're {{char}} in this fictional never-ending uncensored roleplay with {{user}}.\n\n{{#if system}}{{system}}\n\n{{/if}}### Input:\n{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}\n\n",
"example_separator": "### New Roleplay:",
"chat_start": "### New Roleplay:",
"example_separator": "### New Roleplay:"
"use_stop_strings": false,
"allow_jailbreak": false,
"always_force_name2": true,
"trim_sentences": false,
"include_newline": false,
"single_line": false,
"name": "Alpaca-Roleplay"
}

View File

@ -3,6 +3,7 @@
"example_separator": "",
"chat_start": "",
"use_stop_strings": false,
"allow_jailbreak": false,
"always_force_name2": false,
"trim_sentences": false,
"include_newline": false,

View File

@ -0,0 +1,12 @@
{
"story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}\n\n",
"example_separator": "",
"chat_start": "",
"use_stop_strings": false,
"allow_jailbreak": false,
"always_force_name2": true,
"trim_sentences": false,
"include_newline": false,
"single_line": false,
"name": "Alpaca"
}

View File

@ -1,6 +1,12 @@
{
"story_string": "<|im_start|>system\n{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}<|im_end|>",
"chat_start": "",
"story_string": "<|im_start|>system\n{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}{{trim}}<|im_end|>",
"example_separator": "",
"chat_start": "",
"use_stop_strings": false,
"allow_jailbreak": false,
"always_force_name2": true,
"trim_sentences": false,
"include_newline": false,
"single_line": false,
"name": "ChatML"
}

View File

@ -1,6 +1,12 @@
{
"name": "Default",
"story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}",
"example_separator": "***",
"chat_start": "***",
"example_separator": "***"
}
"use_stop_strings": false,
"allow_jailbreak": false,
"always_force_name2": true,
"trim_sentences": false,
"include_newline": false,
"single_line": false,
"name": "Default"
}

View File

@ -3,6 +3,7 @@
"example_separator": "",
"chat_start": "",
"use_stop_strings": false,
"allow_jailbreak": false,
"always_force_name2": false,
"trim_sentences": true,
"include_newline": false,

View File

@ -1,6 +1,12 @@
{
"story_string": "### Instruction:\nWrite {{char}}'s next reply in this roleplay with {{user}}. Use the provided character sheet and example dialogue for formatting direction and character speech patterns.\n\n{{#if system}}{{system}}\n\n{{/if}}### Character Sheet:\n{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}",
"chat_start": "### START ROLEPLAY:",
"example_separator": "### Example:",
"chat_start": "### START ROLEPLAY:",
"use_stop_strings": false,
"allow_jailbreak": false,
"always_force_name2": true,
"trim_sentences": false,
"include_newline": false,
"single_line": false,
"name": "Libra-32B"
}

View File

@ -1,6 +1,12 @@
{
"story_string": "{{system}}\n{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{char}}'s description:{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality:{{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{user}}'s persona: {{persona}}\n{{/if}}",
"chat_start": "This is the history of the roleplay:",
"example_separator": "Example of an interaction:",
"chat_start": "This is the history of the roleplay:",
"use_stop_strings": false,
"allow_jailbreak": false,
"always_force_name2": true,
"trim_sentences": false,
"include_newline": false,
"single_line": false,
"name": "Lightning 1.1"
}
}

View File

@ -0,0 +1,12 @@
{
"story_string": "[INST] <<SYS>>\n{{#if system}}{{system}}\n<</SYS>>\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}{{trim}} [/INST]",
"example_separator": "",
"chat_start": "",
"use_stop_strings": false,
"allow_jailbreak": false,
"always_force_name2": true,
"trim_sentences": false,
"include_newline": false,
"single_line": false,
"name": "Llama 2 Chat"
}

View File

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

View File

@ -1,6 +1,12 @@
{
"story_string": "[INST] {{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}[/INST]",
"chat_start": "",
"story_string": "[INST] {{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}{{trim}} [/INST]",
"example_separator": "Examples:",
"chat_start": "",
"use_stop_strings": false,
"allow_jailbreak": false,
"always_force_name2": true,
"trim_sentences": false,
"include_newline": false,
"single_line": false,
"name": "Mistral"
}
}

View File

@ -1,6 +1,12 @@
{
"name": "NovelAI",
"story_string": "{{#if system}}{{system}}{{/if}}\n{{#if wiBefore}}{{wiBefore}}{{/if}}\n{{#if persona}}{{persona}}{{/if}}\n{{#if description}}{{description}}{{/if}}\n{{#if personality}}Personality: {{personality}}{{/if}}\n{{#if scenario}}Scenario: {{scenario}}{{/if}}\n{{#if wiAfter}}{{wiAfter}}{{/if}}",
"example_separator": "***",
"chat_start": "***",
"example_separator": "***"
"use_stop_strings": false,
"allow_jailbreak": false,
"always_force_name2": true,
"trim_sentences": false,
"include_newline": false,
"single_line": false,
"name": "NovelAI"
}

View File

@ -1,6 +1,12 @@
{
"story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Circumstances and context of the dialogue: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}",
"chat_start": "\nThen the roleplay chat between {{user}} and {{char}} begins.\n",
"example_separator": "This is how {{char}} should talk",
"chat_start": "\nThen the roleplay chat between {{user}} and {{char}} begins.\n",
"use_stop_strings": false,
"allow_jailbreak": false,
"always_force_name2": true,
"trim_sentences": false,
"include_newline": false,
"single_line": false,
"name": "OldDefault"
}

View File

@ -0,0 +1,12 @@
{
"story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}",
"example_separator": "",
"chat_start": "",
"use_stop_strings": false,
"allow_jailbreak": false,
"always_force_name2": true,
"trim_sentences": false,
"include_newline": false,
"single_line": false,
"name": "Pygmalion"
}

View File

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

View File

@ -1,6 +1,12 @@
{
"name": "Pygmalion",
"story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}",
"example_separator": "",
"chat_start": "",
"example_separator": ""
"use_stop_strings": false,
"allow_jailbreak": false,
"always_force_name2": true,
"trim_sentences": false,
"include_newline": false,
"single_line": false,
"name": "Synthia"
}

View File

@ -1,6 +1,12 @@
{
"name": "simple-proxy-for-tavern",
"story_string": "## {{char}}\n- You're \"{{char}}\" in this never-ending roleplay with \"{{user}}\".\n### Input:\n{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}### Response:\n(OOC) Understood. I will take this info into account for the roleplay. (end OOC)",
"example_separator": "### New Roleplay:",
"chat_start": "### New Roleplay:",
"example_separator": "### New Roleplay:"
}
"use_stop_strings": false,
"allow_jailbreak": false,
"always_force_name2": true,
"trim_sentences": false,
"include_newline": false,
"single_line": false,
"name": "simple-proxy-for-tavern"
}

View File

@ -5,7 +5,6 @@
"last_output_sequence": "",
"system_sequence": "",
"stop_sequence": "",
"separator_sequence": "",
"wrap": true,
"macro": true,
"names": false,
@ -14,5 +13,11 @@
"system_sequence_prefix": "",
"system_sequence_suffix": "",
"first_output_sequence": "",
"skip_examples": false,
"output_suffix": "",
"input_suffix": "",
"system_suffix": "",
"user_alignment_message": "",
"system_same_as_user": false,
"name": "Adventure"
}

View File

@ -0,0 +1,23 @@
{
"system_prompt": "Develop the plot slowly, always stay in character. Describe all actions in full, elaborate, explicit, graphic, and vivid detail. Mention all relevant sensory perceptions.",
"input_sequence": "### Instruction:",
"output_sequence": "### Response:",
"last_output_sequence": "### Response (2 paragraphs, engaging, natural, authentic, descriptive, creative):",
"system_sequence": "### Input:",
"stop_sequence": "",
"wrap": true,
"macro": true,
"names": true,
"names_force_groups": true,
"activation_regex": "",
"system_sequence_prefix": "",
"system_sequence_suffix": "",
"first_output_sequence": "",
"skip_examples": false,
"output_suffix": "\n\n",
"input_suffix": "\n\n",
"system_suffix": "\n\n",
"user_alignment_message": "",
"system_same_as_user": false,
"name": "Alpaca-Roleplay"
}

View File

@ -2,16 +2,22 @@
"system_prompt": "Write {{char}}'s next reply in a fictional roleplay chat between {{user}} and {{char}}.\nWrite 1 reply only, italicize actions, and avoid quotation marks. Use markdown. Be proactive, creative, and drive the plot and conversation forward. Include dialog as well as narration.",
"input_sequence": "",
"output_sequence": "",
"first_output_sequence": "<START OF ROLEPLAY>",
"last_output_sequence": "\n### Response:",
"system_sequence_prefix": "",
"system_sequence_suffix": "",
"system_sequence": "",
"stop_sequence": "",
"separator_sequence": "",
"wrap": true,
"macro": true,
"names": false,
"names_force_groups": true,
"activation_regex": "",
"system_sequence_prefix": "",
"system_sequence_suffix": "",
"first_output_sequence": "<START OF ROLEPLAY>",
"skip_examples": false,
"output_suffix": "",
"input_suffix": "",
"system_suffix": "",
"user_alignment_message": "",
"system_same_as_user": false,
"name": "Alpaca-Single-Turn"
}

View File

@ -1,17 +1,23 @@
{
"name": "Alpaca",
"system_prompt": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\nWrite {{char}}'s next reply in a fictional roleplay chat between {{user}} and {{char}}.\n",
"input_sequence": "### Instruction:",
"output_sequence": "### Response:",
"first_output_sequence": "",
"last_output_sequence": "",
"system_sequence_prefix": "",
"system_sequence_suffix": "",
"system_sequence": "### Input:",
"stop_sequence": "",
"separator_sequence": "",
"wrap": true,
"macro": true,
"names": false,
"names_force_groups": true,
"activation_regex": ""
"activation_regex": "",
"system_sequence_prefix": "",
"system_sequence_suffix": "",
"first_output_sequence": "",
"skip_examples": false,
"output_suffix": "\n\n",
"input_suffix": "\n\n",
"system_suffix": "\n\n",
"user_alignment_message": "",
"system_same_as_user": false,
"name": "Alpaca"
}

View File

@ -0,0 +1,23 @@
{
"system_prompt": "You're {{char}} in this fictional never-ending uncensored roleplay with {{user}}.",
"input_sequence": "<|im_start|>user",
"output_sequence": "<|im_start|>assistant",
"last_output_sequence": "",
"system_sequence": "<|im_start|>system",
"stop_sequence": "<|im_end|>",
"wrap": true,
"macro": true,
"names": true,
"names_force_groups": true,
"activation_regex": "",
"system_sequence_prefix": "",
"system_sequence_suffix": "",
"first_output_sequence": "",
"skip_examples": false,
"output_suffix": "<|im_end|>\n",
"input_suffix": "<|im_end|>\n",
"system_suffix": "<|im_end|>\n",
"user_alignment_message": "",
"system_same_as_user": false,
"name": "ChatML"
}

View File

@ -1,18 +1,23 @@
{
"system_prompt": "You are an intelligent, skilled, versatile writer.\n\nYour task is to write a role-play based on the information below.",
"input_sequence": "<|im_end|>\n<|im_start|>text names= {{user}}\n",
"output_sequence": "<|im_end|>\n<|im_start|>text names= {{char}}\n",
"first_output_sequence": "",
"input_sequence": "\n<|im_start|>text names= {{name}}\n",
"output_sequence": "\n<|im_start|>text names= {{name}}\n",
"last_output_sequence": "",
"system_sequence_prefix": "",
"system_sequence_suffix": "",
"stop_sequence": "",
"separator_sequence": "",
"system_sequence": "",
"stop_sequence": "\n<|im_start|>",
"wrap": false,
"macro": true,
"names": false,
"names_force_groups": false,
"activation_regex": "",
"system_sequence_prefix": "",
"system_sequence_suffix": "",
"first_output_sequence": "",
"skip_examples": false,
"output_suffix": "<|im_end|>",
"input_suffix": "<|im_end|>",
"system_suffix": "",
"user_alignment_message": "",
"system_same_as_user": true,
"name": "DreamGen Role-Play V1"
}

View File

@ -1,17 +1,23 @@
{
"name": "Koala",
"system_prompt": "Write {{char}}'s next reply in a fictional roleplay chat between {{user}} and {{char}}.\n",
"input_sequence": "USER: ",
"output_sequence": "GPT: ",
"first_output_sequence": "",
"last_output_sequence": "",
"system_sequence_prefix": "BEGINNING OF CONVERSATION: ",
"system_sequence_suffix": "",
"system_sequence": "",
"stop_sequence": "",
"separator_sequence": "</s>",
"wrap": false,
"macro": true,
"names": false,
"names_force_groups": true,
"activation_regex": ""
}
"activation_regex": "",
"system_sequence_prefix": "BEGINNING OF CONVERSATION: ",
"system_sequence_suffix": "",
"first_output_sequence": "",
"skip_examples": false,
"output_suffix": "</s>",
"input_suffix": "",
"system_suffix": "",
"user_alignment_message": "",
"system_same_as_user": true,
"name": "Koala"
}

View File

@ -1,17 +1,23 @@
{
"wrap": true,
"names": true,
"system_prompt": "Avoid repetition, don't loop. Develop the plot slowly, always stay in character. Describe all actions in full, elaborate, explicit, graphic, and vivid detail. Mention all relevant sensory perceptions.",
"system_sequence_prefix": "",
"stop_sequence": "",
"input_sequence": "",
"output_sequence": "",
"separator_sequence": "",
"macro": true,
"names_force_groups": true,
"last_output_sequence": "\n### Response:",
"system_sequence": "",
"stop_sequence": "",
"wrap": true,
"macro": true,
"names": true,
"names_force_groups": true,
"activation_regex": "",
"first_output_sequence": "",
"system_sequence_prefix": "",
"system_sequence_suffix": "",
"first_output_sequence": "",
"skip_examples": false,
"output_suffix": "",
"input_suffix": "",
"system_suffix": "",
"user_alignment_message": "",
"system_same_as_user": false,
"name": "Libra-32B"
}

View File

@ -1,18 +1,23 @@
{
"wrap": true,
"names": false,
"system_prompt": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nTake the role of {{char}} in a play that leaves a lasting impression on {{user}}. Write {{char}}'s next reply.\nNever skip or gloss over {{char}}s actions. Progress the scene at a naturally slow pace.\n\n",
"system_sequence": "",
"stop_sequence": "",
"input_sequence": "### Instruction:",
"output_sequence": "### Response: (length = unlimited)",
"separator_sequence": "",
"macro": true,
"names_force_groups": true,
"last_output_sequence": "",
"system_sequence": "",
"stop_sequence": "",
"wrap": true,
"macro": true,
"names": false,
"names_force_groups": true,
"activation_regex": "",
"system_sequence_prefix": "",
"system_sequence_suffix": "",
"first_output_sequence": "",
"activation_regex": "",
"skip_examples": false,
"output_suffix": "",
"input_suffix": "",
"system_suffix": "",
"user_alignment_message": "",
"system_same_as_user": true,
"name": "Lightning 1.1"
}
}

View File

@ -0,0 +1,23 @@
{
"system_prompt": "Write {{char}}'s next reply in this fictional roleplay with {{user}}.",
"input_sequence": "[INST] ",
"output_sequence": "",
"last_output_sequence": "",
"system_sequence": "",
"stop_sequence": "",
"wrap": false,
"macro": true,
"names": false,
"names_force_groups": true,
"activation_regex": "",
"system_sequence_prefix": "",
"system_sequence_suffix": "",
"first_output_sequence": "",
"skip_examples": false,
"output_suffix": "\n",
"input_suffix": " [/INST]\n",
"system_suffix": "",
"user_alignment_message": "Let's get started. Please respond based on the information and instructions provided above.",
"system_same_as_user": true,
"name": "Llama 2 Chat"
}

View File

@ -1,17 +1,23 @@
{
"name": "Metharme",
"system_prompt": "Enter roleplay mode. You must act as {{char}}, whose persona follows:",
"input_sequence": "<|user|>",
"output_sequence": "<|model|>",
"first_output_sequence": "",
"last_output_sequence": "",
"system_sequence_prefix": "<|system|>",
"system_sequence_suffix": "",
"system_sequence": "",
"stop_sequence": "</s>",
"separator_sequence": "",
"wrap": false,
"macro": true,
"names": false,
"names_force_groups": true,
"activation_regex": ""
}
"activation_regex": "",
"system_sequence_prefix": "<|system|>",
"system_sequence_suffix": "",
"first_output_sequence": "",
"skip_examples": false,
"output_suffix": "",
"input_suffix": "",
"system_suffix": "",
"user_alignment_message": "",
"system_same_as_user": true,
"name": "Metharme"
}

View File

@ -1,17 +1,23 @@
{
"wrap": false,
"names": true,
"system_prompt": "Write {{char}}'s next reply in this fictional roleplay with {{user}}.",
"system_sequence_prefix": "",
"stop_sequence": "",
"input_sequence": "[INST] ",
"output_sequence": " [/INST]\n",
"separator_sequence": "\n",
"macro": true,
"names_force_groups": true,
"output_sequence": "",
"last_output_sequence": "",
"system_sequence": "",
"stop_sequence": "",
"wrap": false,
"macro": true,
"names": true,
"names_force_groups": true,
"activation_regex": "",
"first_output_sequence": "\n",
"system_sequence_prefix": "",
"system_sequence_suffix": "",
"first_output_sequence": "",
"skip_examples": false,
"output_suffix": "\n",
"input_suffix": " [/INST]\n",
"system_suffix": "",
"user_alignment_message": "Let's get started. Please respond based on the information and instructions provided above.",
"system_same_as_user": true,
"name": "Mistral"
}
}

View File

@ -1,17 +1,23 @@
{
"name": "OpenOrca-OpenChat",
"system_prompt": "You are a helpful assistant. Please answer truthfully and write out your thinking step by step to be sure you get the right answer. If you make a mistake or encounter an error in your thinking, say so out loud and attempt to correct it. If you don't know or aren't sure about something, say so clearly. You will act as a professional logician, mathematician, and physicist. You will also act as the most appropriate type of expert to answer any particular question or solve the relevant problem; state which expert type your are, if so. Also think of any particular named expert that would be ideal to answer the relevant question or solve the relevant problem; name and act as them, if appropriate.\n",
"input_sequence": "User: ",
"output_sequence": "<|end_of_turn|>\nAssistant: ",
"first_output_sequence": "",
"input_sequence": "\nUser: ",
"output_sequence": "\nAssistant: ",
"last_output_sequence": "",
"system_sequence_prefix": "",
"system_sequence_suffix": "",
"system_sequence": "",
"stop_sequence": "",
"separator_sequence": "<|end_of_turn|>\n",
"wrap": false,
"macro": true,
"names": false,
"names_force_groups": true,
"activation_regex": ""
}
"activation_regex": "",
"system_sequence_prefix": "",
"system_sequence_suffix": "",
"first_output_sequence": "",
"skip_examples": false,
"output_suffix": "<|end_of_turn|>",
"input_suffix": "<|end_of_turn|>",
"system_suffix": "",
"user_alignment_message": "",
"system_same_as_user": false,
"name": "OpenOrca-OpenChat"
}

View File

@ -1,17 +1,23 @@
{
"name": "Pygmalion",
"system_prompt": "Enter RP mode. You shall reply to {{user}} while staying in character. Your responses must be detailed, creative, immersive, and drive the scenario forward. You will follow {{char}}'s persona.",
"input_sequence": "<|user|>",
"output_sequence": "<|model|>",
"first_output_sequence": "",
"last_output_sequence": "",
"system_sequence_prefix": "<|system|>",
"system_sequence_suffix": "",
"system_sequence": "",
"stop_sequence": "<|user|>",
"separator_sequence": "",
"wrap": false,
"macro": true,
"names": true,
"names_force_groups": true,
"activation_regex": ""
}
"activation_regex": "",
"system_sequence_prefix": "<|system|>",
"system_sequence_suffix": "",
"first_output_sequence": "",
"skip_examples": false,
"output_suffix": "",
"input_suffix": "",
"system_suffix": "",
"user_alignment_message": "",
"system_same_as_user": true,
"name": "Pygmalion"
}

View File

@ -5,7 +5,6 @@
"last_output_sequence": "",
"system_sequence": "",
"stop_sequence": "",
"separator_sequence": "",
"wrap": true,
"macro": true,
"names": false,
@ -14,5 +13,11 @@
"system_sequence_prefix": "",
"system_sequence_suffix": "",
"first_output_sequence": "",
"skip_examples": false,
"output_suffix": "",
"input_suffix": "",
"system_suffix": "",
"user_alignment_message": "",
"system_same_as_user": false,
"name": "Story"
}

View File

@ -1,17 +1,23 @@
{
"wrap": false,
"names": false,
"system_prompt": "Elaborate on the topic using a Tree of Thoughts and backtrack when necessary to construct a clear, cohesive Chain of Thought reasoning. Always answer without hesitation.",
"system_sequence_prefix": "SYSTEM: ",
"stop_sequence": "",
"input_sequence": "USER: ",
"output_sequence": "\nASSISTANT: ",
"separator_sequence": "\n",
"macro": true,
"names_force_groups": true,
"output_sequence": "ASSISTANT: ",
"last_output_sequence": "",
"system_sequence": "SYSTEM: ",
"stop_sequence": "",
"wrap": false,
"macro": true,
"names": false,
"names_force_groups": true,
"activation_regex": "",
"first_output_sequence": "ASSISTANT: ",
"system_sequence_prefix": "SYSTEM: ",
"system_sequence_suffix": "",
"first_output_sequence": "",
"skip_examples": false,
"output_suffix": "\n",
"input_suffix": "\n",
"system_suffix": "\n",
"user_alignment_message": "Let's get started. Please respond based on the information and instructions provided above.",
"system_same_as_user": false,
"name": "Synthia"
}
}

View File

@ -1,17 +1,23 @@
{
"name": "Vicuna 1.0",
"system_prompt": "A chat between a curious human and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the human's questions.\n\nWrite {{char}}'s next reply in a fictional roleplay chat between {{user}} and {{char}}.\n",
"input_sequence": "### Human:",
"output_sequence": "### Assistant:",
"first_output_sequence": "",
"last_output_sequence": "",
"system_sequence_prefix": "",
"system_sequence_suffix": "",
"system_sequence": "",
"stop_sequence": "",
"separator_sequence": "",
"wrap": true,
"macro": true,
"names": false,
"names_force_groups": true,
"activation_regex": ""
}
"activation_regex": "",
"system_sequence_prefix": "",
"system_sequence_suffix": "",
"first_output_sequence": "",
"skip_examples": false,
"output_suffix": "",
"input_suffix": "",
"system_suffix": "",
"user_alignment_message": "",
"system_same_as_user": true,
"name": "Vicuna 1.0"
}

View File

@ -1,17 +1,23 @@
{
"name": "Vicuna 1.1",
"system_prompt": "A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.\n\nWrite {{char}}'s next reply in a fictional roleplay chat between {{user}} and {{char}}.\n",
"input_sequence": "\nUSER: ",
"output_sequence": "\nASSISTANT: ",
"first_output_sequence": "",
"last_output_sequence": "",
"system_sequence_prefix": "BEGINNING OF CONVERSATION:",
"system_sequence_suffix": "",
"system_sequence": "",
"stop_sequence": "",
"separator_sequence": "</s>",
"wrap": false,
"macro": true,
"names": false,
"names_force_groups": true,
"activation_regex": ""
}
"activation_regex": "",
"system_sequence_prefix": "BEGINNING OF CONVERSATION:",
"system_sequence_suffix": "",
"first_output_sequence": "",
"skip_examples": false,
"output_suffix": "</s>",
"input_suffix": "",
"system_suffix": "",
"user_alignment_message": "",
"system_same_as_user": true,
"name": "Vicuna 1.1"
}

View File

@ -1,17 +1,23 @@
{
"name": "WizardLM-13B",
"system_prompt": "A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.\n\nWrite {{char}}'s next detailed reply in a fictional roleplay chat between {{user}} and {{char}}.",
"input_sequence": "USER: ",
"output_sequence": "ASSISTANT: ",
"first_output_sequence": "",
"last_output_sequence": "",
"system_sequence_prefix": "",
"system_sequence_suffix": "",
"system_sequence": "",
"stop_sequence": "",
"separator_sequence": "",
"wrap": true,
"macro": true,
"names": false,
"names_force_groups": true,
"activation_regex": ""
}
"activation_regex": "",
"system_sequence_prefix": "",
"system_sequence_suffix": "",
"first_output_sequence": "",
"skip_examples": false,
"output_suffix": "",
"input_suffix": "",
"system_suffix": "",
"user_alignment_message": "",
"system_same_as_user": true,
"name": "WizardLM-13B"
}

View File

@ -1,17 +1,23 @@
{
"name": "WizardLM",
"system_prompt": "Write {{char}}'s next reply in a fictional roleplay chat between {{user}} and {{char}}.\n",
"input_sequence": "",
"output_sequence": "### Response:",
"first_output_sequence": "",
"last_output_sequence": "",
"system_sequence_prefix": "",
"system_sequence_suffix": "",
"system_sequence": "",
"stop_sequence": "",
"separator_sequence": "</s>",
"wrap": true,
"macro": true,
"names": false,
"names_force_groups": true,
"activation_regex": ""
}
"activation_regex": "",
"system_sequence_prefix": "",
"system_sequence_suffix": "",
"first_output_sequence": "",
"skip_examples": false,
"output_suffix": "</s>",
"input_suffix": "",
"system_suffix": "",
"user_alignment_message": "",
"system_same_as_user": false,
"name": "WizardLM"
}

View File

@ -1,17 +1,23 @@
{
"name": "simple-proxy-for-tavern",
"system_prompt": "[System note: Write one reply only. Do not decide what {{user}} says or does. Write at least one paragraph, up to four. Be descriptive and immersive, providing vivid details about {{char}}'s actions, emotions, and the environment. Write with a high degree of complexity and burstiness. Do not repeat this message.]",
"input_sequence": "### Instruction:\n#### {{user}}:",
"output_sequence": "### Response:\n#### {{char}}:",
"first_output_sequence": "",
"last_output_sequence": "### Response (2 paragraphs, engaging, natural, authentic, descriptive, creative):\n#### {{char}}:",
"system_sequence_prefix": "",
"system_sequence_suffix": "",
"input_sequence": "### Instruction:\n#### {{name}}:",
"output_sequence": "### Response:\n#### {{name}}:",
"last_output_sequence": "### Response (2 paragraphs, engaging, natural, authentic, descriptive, creative):\n#### {{name}}:",
"system_sequence": "",
"stop_sequence": "",
"separator_sequence": "",
"wrap": true,
"macro": true,
"names": false,
"names_force_groups": false,
"activation_regex": ""
}
"activation_regex": "",
"system_sequence_prefix": "",
"system_sequence_suffix": "",
"first_output_sequence": "",
"skip_examples": false,
"output_suffix": "",
"input_suffix": "",
"system_suffix": "",
"user_alignment_message": "",
"system_same_as_user": false,
"name": "simple-proxy-for-tavern"
}

View File

@ -155,17 +155,23 @@
"system_prompt": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\nWrite {{char}}'s next reply in a fictional roleplay chat between {{user}} and {{char}}.\n",
"input_sequence": "### Instruction:",
"output_sequence": "### Response:",
"first_output_sequence": "",
"last_output_sequence": "",
"system_sequence_prefix": "",
"system_sequence_suffix": "",
"system_sequence": "### Input:",
"stop_sequence": "",
"separator_sequence": "",
"wrap": true,
"macro": true,
"names": false,
"names_force_groups": true,
"activation_regex": ""
"activation_regex": "",
"system_sequence_prefix": "",
"system_sequence_suffix": "",
"first_output_sequence": "",
"skip_examples": false,
"output_suffix": "\n\n",
"input_suffix": "\n\n",
"system_suffix": "\n\n",
"user_alignment_message": "",
"system_same_as_user": false
},
"default_context": "Default",
"context": {

0
public/context/.gitkeep Normal file
View File

View File

@ -456,6 +456,7 @@
input:disabled,
textarea:disabled {
cursor: not-allowed;
filter: brightness(0.5);
}
.debug-red {

View File

@ -139,11 +139,13 @@
cursor: pointer;
opacity: 0.6;
filter: brightness(0.8);
}
.rm_tag_filter .tag.actionable {
transition: opacity 200ms;
}
.rm_tag_filter .tag:hover {
opacity: 1;
filter: brightness(1);
}
@ -230,18 +232,16 @@
.rm_tag_bogus_drilldown .tag:not(:first-child) {
position: relative;
margin-left: calc(var(--mainFontSize) * 2);
margin-left: 1em;
}
.rm_tag_bogus_drilldown .tag:not(:first-child)::before {
font-family: 'Font Awesome 6 Free';
content: "\f054";
position: absolute;
left: calc(var(--mainFontSize) * -2);
top: -1px;
content: "\21E8";
font-size: calc(var(--mainFontSize) * 2);
left: -1em;
top: auto;
color: var(--SmartThemeBodyColor);
line-height: calc(var(--mainFontSize) * 1.3);
text-align: center;
text-shadow: 1px 1px 0px black,
-1px -1px 0px black,
-1px 1px 0px black,

12
public/img/cohere.svg Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg width="47.403999mm" height="47.58918mm" viewBox="0 0 47.403999 47.58918" version="1.1" id="svg1" xml:space="preserve" inkscape:version="1.3 (0e150ed, 2023-07-21)" sodipodi:docname="cohere.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview id="namedview1" pagecolor="#ffffff" bordercolor="#000000" borderopacity="0.25" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:document-units="mm" inkscape:clip-to-page="false" inkscape:zoom="0.69294747" inkscape:cx="67.826209" inkscape:cy="74.320208" inkscape:window-width="1280" inkscape:window-height="688" inkscape:window-x="0" inkscape:window-y="25" inkscape:window-maximized="1" inkscape:current-layer="svg1" />
<defs id="defs1" />
<path id="path7" fill="currentColor" d="m 88.320761,61.142067 c -5.517973,0.07781 -11.05887,-0.197869 -16.558458,0.321489 -6.843243,0.616907 -12.325958,7.018579 -12.29857,13.807832 -0.139102,5.883715 3.981307,11.431418 9.578012,13.180923 3.171819,1.100505 6.625578,1.228214 9.855341,0.291715 3.455286,-0.847586 6.634981,-2.530123 9.969836,-3.746213 4.659947,-1.981154 9.49864,-3.782982 13.612498,-6.795254 3.80146,-2.664209 4.45489,-8.316688 2.00772,-12.1054 -1.74871,-3.034851 -5.172793,-4.896444 -8.663697,-4.741041 -2.49833,-0.140901 -5.000698,-0.196421 -7.502682,-0.214051 z m 7.533907,25.636161 c -3.334456,0.15056 -6.379399,1.79356 -9.409724,3.054098 -2.379329,1.032102 -4.911953,2.154839 -6.246333,4.528375 -2.118159,3.080424 -2.02565,7.404239 0.309716,10.346199 1.877703,2.72985 5.192756,4.03199 8.428778,3.95319 3.087361,0.0764 6.223907,0.19023 9.275119,-0.34329 5.816976,-1.32118 9.855546,-7.83031 8.101436,-13.600351 -1.30234,-4.509858 -5.762,-7.905229 -10.458992,-7.938221 z m -28.342456,4.770768 c -4.357593,-0.129828 -8.148265,3.780554 -8.168711,8.09095 -0.296313,4.101314 2.711752,8.289544 6.873869,8.869074 4.230007,0.80322 8.929483,-2.66416 9.017046,-7.07348 0.213405,-2.445397 0.09191,-5.152074 -1.705492,-7.039611 -1.484313,-1.763448 -3.717801,-2.798154 -6.016712,-2.846933 z" transform="translate(-59.323375,-61.136763)" />
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -458,7 +458,7 @@
</span>
</div>
</div>
<div class="range-block" data-source="openai,claude,windowai,openrouter,ai21,scale,makersuite,mistralai,custom">
<div class="range-block" data-source="openai,claude,windowai,openrouter,ai21,scale,makersuite,mistralai,custom,cohere">
<div class="range-block-title" data-i18n="Temperature">
Temperature
</div>
@ -471,7 +471,7 @@
</div>
</div>
</div>
<div data-newbie-hidden class="range-block" data-source="openai,openrouter,ai21,custom">
<div data-newbie-hidden class="range-block" data-source="openai,openrouter,ai21,custom,cohere">
<div class="range-block-title" data-i18n="Frequency Penalty">
Frequency Penalty
</div>
@ -484,7 +484,7 @@
</div>
</div>
</div>
<div data-newbie-hidden class="range-block" data-source="openai,openrouter,ai21,custom">
<div data-newbie-hidden class="range-block" data-source="openai,openrouter,ai21,custom,cohere">
<div class="range-block-title" data-i18n="Presence Penalty">
Presence Penalty
</div>
@ -510,20 +510,20 @@
</div>
</div>
</div>
<div data-newbie-hidden class="range-block" data-source="claude,openrouter,ai21,makersuite">
<div data-newbie-hidden class="range-block" data-source="claude,openrouter,ai21,makersuite,cohere">
<div class="range-block-title" data-i18n="Top K">
Top K
</div>
<div class="range-block-range-and-counter">
<div class="range-block-range">
<input type="range" id="top_k_openai" name="volume" min="0" max="200" step="1">
<input type="range" id="top_k_openai" name="volume" min="0" max="500" step="1">
</div>
<div class="range-block-counter">
<input type="number" min="0" max="200" step="1" data-for="top_k_openai" id="top_k_counter_openai">
</div>
</div>
</div>
<div data-newbie-hidden class="range-block" data-source="openai,claude,openrouter,ai21,scale,makersuite,mistralai,custom">
<div data-newbie-hidden class="range-block" data-source="openai,claude,openrouter,ai21,scale,makersuite,mistralai,custom,cohere">
<div class="range-block-title" data-i18n="Top-p">
Top P
</div>
@ -759,7 +759,7 @@
</div>
</div>
</div>
<div data-newbie-hidden class="range-block" data-source="openai,openrouter,mistralai,custom">
<div data-newbie-hidden class="range-block" data-source="openai,openrouter,mistralai,custom,cohere">
<div class="range-block-title justifyLeft" data-i18n="Seed">
Seed
</div>
@ -1516,6 +1516,17 @@
</div>
</div>
</div>
<div data-newbie-hidden id="json_schema_block" data-tg-type="tabby" class="wide100p">
<hr class="wide100p">
<h4 class="wide100p textAlignCenter"><span data-i18n="JSON Schema">JSON Schema</span>
<a href="https://json-schema.org/learn/getting-started-step-by-step" target="_blank">
<small>
<div class="fa-solid fa-circle-question note-link-span"></div>
</small>
</a>
</h4>
<textarea id="tabby_json_schema" rows="4" class="text_pole textarea_compact monospace" data-i18n="[placeholder]Type in the desired JSON schema" placeholder="Type in the desired JSON schema"></textarea>
</div>
<div data-newbie-hidden id="grammar_block_ooba" class="wide100p">
<hr class="wide100p">
<h4 class="wide100p textAlignCenter">
@ -2151,6 +2162,15 @@
ggerganov/llama.cpp (inference server)
</a>
</div>
<h4 data-i18n="API key (optional)">API key (optional)</h4>
<div class="flex-container">
<input id="api_key_llamacpp" name="api_key_llamacpp" class="text_pole flex1 wide100p" maxlength="500" size="35" type="text" autocomplete="off">
<div title="Clear your API key" data-i18n="[title]Clear your API key" class="menu_button fa-solid fa-circle-xmark clear-api-key" data-key="api_key_llamacpp">
</div>
</div>
<div data-for="api_key_llamacpp" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you reload the page.">
For privacy reasons, your API key will be hidden after you reload the page.
</div>
<div class="flex1">
<h4 data-i18n="API url">API URL</h4>
<small data-i18n="Example: 127.0.0.1:8080">Example: http://127.0.0.1:8080</small>
@ -2250,15 +2270,20 @@
Chat Completion Source
</h4>
<select id="chat_completion_source">
<option value="openai">OpenAI</option>
<option value="windowai">Window AI</option>
<option value="openrouter">OpenRouter</option>
<option value="claude">Claude</option>
<option value="scale">Scale</option>
<option value="ai21">AI21</option>
<option value="makersuite">Google MakerSuite</option>
<option value="mistralai">MistralAI</option>
<option value="custom">Custom (OpenAI-compatible)</option>
<optgroup>
<option value="openai">OpenAI</option>
<option value="custom">Custom (OpenAI-compatible)</option>
</optgroup>
<optgroup>
<option value="ai21">AI21</option>
<option value="claude">Claude</option>
<option value="cohere">Cohere</option>
<option value="makersuite">Google MakerSuite</option>
<option value="mistralai">MistralAI</option>
<option value="openrouter">OpenRouter</option>
<option value="scale">Scale</option>
<option value="windowai">Window AI</option>
</optgroup>
</select>
<div data-newbie-hidden class="inline-drawer wide100p" data-source="openai,claude,mistralai">
<div class="inline-drawer-toggle inline-drawer-header">
@ -2650,6 +2675,30 @@
</select>
</div>
</form>
<form id="cohere_form" data-source="cohere" action="javascript:void(null);" method="post" enctype="multipart/form-data">
<h4 data-i18n="Cohere API Key">Cohere API Key</h4>
<div class="flex-container">
<input id="api_key_cohere" name="api_key_cohere" class="text_pole flex1" maxlength="500" value="" type="text" autocomplete="off">
<div title="Clear your API key" data-i18n="[title]Clear your API key" class="menu_button fa-solid fa-circle-xmark clear-api-key" data-key="api_key_cohere"></div>
</div>
<div data-for="api_key_cohere" class="neutral_warning">
For privacy reasons, your API key will be hidden after you reload the page.
</div>
<div>
<h4 data-i18n="Cohere Model">Cohere Model</h4>
<select id="model_cohere_select">
<optgroup label="Stable">
<option value="command-light">command-light</option>
<option value="command">command</option>
<option value="command-r">command-r</option>
</optgroup>
<optgroup label="Nightly">
<option value="command-light-nightly">command-light-nightly</option>
<option value="command-nightly">command-nightly</option>
</optgroup>
</select>
</div>
</form>
<form id="custom_form" data-source="custom">
<h4 data-i18n="Custom Endpoint (Base URL)">Custom Endpoint (Base URL)</h4>
<div class="flex-container">
@ -2715,8 +2764,14 @@
<div class="flex-container">
<div id="PygOverrides">
<div>
<h4 data-i18n="Context Template">
Context Template
<h4 class="standoutHeader title_restorable">
<span data-i18n="Context Template">Context Template</span>
<div class="flex-container">
<i data-newbie-hidden data-preset-manager-import="context" class="margin0 menu_button fa-solid fa-file-import" title="Import preset" data-i18n="[title]Import preset"></i>
<i data-newbie-hidden data-preset-manager-export="context" class="margin0 menu_button fa-solid fa-file-export" title="Export preset" data-i18n="[title]Export preset"></i>
<i data-newbie-hidden data-preset-manager-restore="context" class="margin0 menu_button fa-solid fa-recycle" title="Restore current preset" data-i18n="[title]Restore current preset"></i>
<i data-newbie-hidden id="context_delete_preset" data-preset-manager-delete="context" class="margin0 menu_button fa-solid fa-trash-can" title="Delete the preset" data-i18n="[title]Delete the preset"></i>
</div>
</h4>
<div class="flex-container">
<select id="context_presets" data-preset-manager-for="context" class="flex1 text_pole"></select>
@ -2724,9 +2779,6 @@
<i id="context_set_default" class="menu_button fa-solid fa-heart" title="Auto-select this preset for Instruct Mode." data-i18n="[title]Auto-select this preset for Instruct Mode"></i>
<i data-newbie-hidden data-preset-manager-update="context" class="menu_button fa-solid fa-save" title="Update current preset" data-i18n="[title]Update current preset"></i>
<i data-newbie-hidden data-preset-manager-new="context" class="menu_button fa-solid fa-file-circle-plus" title="Save preset as" data-i18n="[title]Save preset as"></i>
<i data-newbie-hidden data-preset-manager-import="context" class="menu_button fa-solid fa-file-import" title="Import preset" data-i18n="[title]Import preset"></i>
<i data-newbie-hidden data-preset-manager-export="context" class="menu_button fa-solid fa-file-export" title="Export preset" data-i18n="[title]Export preset"></i>
<i data-newbie-hidden id="context_delete_preset" data-preset-manager-delete="context" class="menu_button fa-solid fa-trash-can" title="Delete the preset" data-i18n="[title]Delete the preset"></i>
</div>
<div data-newbie-hidden>
<label for="context_story_string">
@ -2799,10 +2851,19 @@
</div>
</div>
<div>
<h4 data-i18n="Instruct Mode">Instruct Mode
<a href="https://docs.sillytavern.app/usage/core-concepts/instructmode/" class="notes-link" target="_blank">
<span class="fa-solid fa-circle-question note-link-span"></span>
</a>
<h4 class="standoutHeader title_restorable">
<div>
<span data-i18n="Instruct Mode">Instruct Mode</span>
<a href="https://docs.sillytavern.app/usage/core-concepts/instructmode/" class="notes-link" target="_blank">
<span class="fa-solid fa-circle-question note-link-span"></span>
</a>
</div>
<div class="flex-container">
<i data-newbie-hidden data-preset-manager-import="instruct" class="margin0 menu_button fa-solid fa-file-import" title="Import preset" data-i18n="[title]Import preset"></i>
<i data-newbie-hidden data-preset-manager-export="instruct" class="margin0 menu_button fa-solid fa-file-export" title="Export preset" data-i18n="[title]Export preset"></i>
<i data-newbie-hidden data-preset-manager-restore="instruct" class="margin0 menu_button fa-solid fa-recycle" title="Restore current preset" data-i18n="[title]Restore current preset"></i>
<i data-newbie-hidden data-preset-manager-delete="instruct" class="margin0 menu_button fa-solid fa-trash-can" title="Delete the preset" data-i18n="[title]Delete the preset"></i>
</div>
</h4>
<div class="flex-container">
<label for="instruct_enabled" class="checkbox_label flex1">
@ -2823,9 +2884,6 @@
<i id="instruct_set_default" class="menu_button fa-solid fa-heart" title="Auto-select this preset on API connection." data-i18n="[title]Auto-select this preset on API connection"></i>
<i data-newbie-hidden data-preset-manager-update="instruct" class="menu_button fa-solid fa-save" title="Update current preset" data-i18n="[title]Update current preset"></i>
<i data-newbie-hidden data-preset-manager-new="instruct" class="menu_button fa-solid fa-file-circle-plus" title="Save preset as" data-i18n="[title]Save preset as"></i>
<i data-newbie-hidden data-preset-manager-import="instruct" class="menu_button fa-solid fa-file-import" title="Import preset" data-i18n="[title]Import preset"></i>
<i data-newbie-hidden data-preset-manager-export="instruct" class="menu_button fa-solid fa-file-export" title="Export preset" data-i18n="[title]Export preset"></i>
<i data-newbie-hidden data-preset-manager-delete="instruct" class="menu_button fa-solid fa-trash-can" title="Delete the preset" data-i18n="[title]Delete the preset"></i>
</div>
<label data-newbie-hidden>
<small data-i18n="Activation Regex">
@ -2869,36 +2927,105 @@
<div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
</div>
<div class="inline-drawer-content">
<h5 class="textAlignCenter" data-i18n="System Prompt Wrapping">
System Prompt Wrapping
</h5>
<div class="flex-container">
<div class="flex1">
<div class="flex1" title="Inserted before a System prompt.">
<label for="instruct_system_sequence_prefix">
<small data-i18n="System Prompt Prefix">System Prompt Prefix</small>
</label>
<div>
<textarea id="instruct_system_sequence_prefix" class="text_pole textarea_compact autoSetHeight" maxlength="2000" placeholder="&mdash;" rows="1"></textarea>
</div>
</div>
<div class="flex1" title="Inserted after a System prompt.">
<label for="instruct_system_sequence_suffix">
<small data-i18n="System Prompt Suffix">System Prompt Suffix</small>
</label>
<div>
<textarea id="instruct_system_sequence_suffix" class="text_pole wide100p textarea_compact autoSetHeight" maxlength="2000" placeholder="&mdash;" rows="1"></textarea>
</div>
</div>
</div>
<h5 class="textAlignCenter" data-i18n="Chat Messages Wrapping">
Chat Messages Wrapping
</h5>
<div class="flex-container">
<div class="flex1" title="Inserted before a User message and as a last prompt line when impersonating.">
<label for="instruct_input_sequence">
<small data-i18n="Input Sequence">Input Sequence</small>
<small data-i18n="User Message Prefix">User Message Prefix</small>
</label>
<div>
<textarea id="instruct_input_sequence" class="text_pole textarea_compact autoSetHeight" maxlength="2000" placeholder="&mdash;" rows="1"></textarea>
</div>
</div>
<div class="flex1">
<div class="flex1" title="Inserted after a User message.">
<label for="instruct_input_suffix">
<small data-i18n="User Message Suffix">User Message Suffix</small>
</label>
<div>
<textarea id="instruct_input_suffix" class="text_pole wide100p textarea_compact autoSetHeight" maxlength="2000" placeholder="&mdash;" rows="1"></textarea>
</div>
</div>
</div>
<div class="flex-container">
<div class="flex1" title="Inserted before an Assistant message and as a last prompt line when generating an AI reply.">
<label for="instruct_output_sequence">
<small data-i18n="Output Sequence">Output Sequence</small>
<small data-i18n="Assistant Message Prefix">Assistant Message Prefix</small>
</label>
<div>
<textarea id="instruct_output_sequence" class="text_pole wide100p textarea_compact autoSetHeight" maxlength="2000" placeholder="&mdash;" rows="1"></textarea>
</div>
</div>
<div class="flex1" title="Inserted after an Assistant message.">
<label for="instruct_output_suffix">
<small data-i18n="Assistant Message Suffix">Assistant Message Suffix</small>
</label>
<div>
<textarea id="instruct_output_suffix" class="text_pole wide100p textarea_compact autoSetHeight" maxlength="2000" placeholder="&mdash;" rows="1"></textarea>
</div>
</div>
</div>
<div class="flex-container">
<div class="flex1">
<div class="flex1" title="Inserted before a System (added by slash commands or extensions) message.">
<label for="instruct_system_sequence">
<small data-i18n="System Message Prefix">System Message Prefix</small>
</label>
<div>
<textarea id="instruct_system_sequence" class="text_pole textarea_compact autoSetHeight" maxlength="2000" placeholder="&mdash;" rows="1"></textarea>
</div>
</div>
<div class="flex1" title="Inserted after a System message.">
<label for="instruct_system_suffix">
<small data-i18n="System Message Suffix">System Message Suffix</small>
</label>
<div>
<textarea id="instruct_system_suffix" class="text_pole wide100p textarea_compact autoSetHeight" maxlength="2000" placeholder="&mdash;" rows="1"></textarea>
</div>
</div>
<div class="flexBasis100p" title="If enabled, System Sequences will be the same as User Sequences.">
<label class="checkbox_label" for="instruct_system_same_as_user">
<input id="instruct_system_same_as_user" type="checkbox" />
<small data-i18n="System same as User">System same as User</small>
</label>
</div>
</div>
<h5 class="textAlignCenter" data-i18n="Misc. Sequences">
Misc. Sequences
</h5>
<div class="flex-container">
<div class="flex1" title="Inserted before the first Assistant's message.">
<label for="instruct_first_output_sequence">
<small data-i18n="First Output Sequence">First Output Sequence</small>
<small data-i18n="First Assistant Prefix">First Assistant Prefix</small>
</label>
<div>
<textarea id="instruct_first_output_sequence" class="text_pole textarea_compact autoSetHeight" maxlength="2000" placeholder="&mdash;" rows="1"></textarea>
</div>
</div>
<div class="flex1">
<div class="flex1" title="Inserted before the last Assistant's message or as a last prompt line when generating an AI reply (except a neutral/system role).">
<label for="instruct_last_output_sequence">
<small data-i18n="Last Output Sequence">Last Output Sequence</small>
<small data-i18n="Last Assistant Prefix">Last Assistant Prefix</small>
</label>
<div>
<textarea id="instruct_last_output_sequence" class="text_pole wide100p textarea_compact autoSetHeight" maxlength="2000" placeholder="&mdash;" rows="1"></textarea>
@ -2906,25 +3033,15 @@
</div>
</div>
<div class="flex-container">
<div class="flex1">
<label for="instruct_system_sequence_prefix">
<small data-i18n="System Sequence Prefix">System Sequence Prefix</small>
<div class="flex1" title="Will be inserted at the start of the chat history if it doesn't start with a User message.">
<label for="instruct_user_alignment_message">
<small data-i18n="User Filler Message">User Filler Message</small>
</label>
<div>
<textarea id="instruct_system_sequence_prefix" class="text_pole textarea_compact autoSetHeight" maxlength="2000" placeholder="&mdash;" rows="1"></textarea>
<textarea id="instruct_user_alignment_message" class="text_pole textarea_compact autoSetHeight" maxlength="2000" placeholder="&mdash;" rows="1"></textarea>
</div>
</div>
<div class="flex1">
<label for="instruct_system_sequence_suffix">
<small data-i18n="System Sequence Suffix">System Sequence Suffix</small>
</label>
<div>
<textarea id="instruct_system_sequence_suffix" class="text_pole wide100p textarea_compact autoSetHeight" maxlength="2000" placeholder="&mdash;" rows="1"></textarea>
</div>
</div>
</div>
<div class="flex-container">
<div class="flex1">
<div class="flex1" title="If a stop sequence is generated, everything past it will be removed from the output (inclusive).">
<label for="instruct_stop_sequence">
<small data-i18n="Stop Sequence">Stop Sequence</small>
</label>
@ -2932,14 +3049,6 @@
<textarea id="instruct_stop_sequence" class="text_pole textarea_compact autoSetHeight" maxlength="2000" placeholder="&mdash;" rows="1"></textarea>
</div>
</div>
<div class="flex1">
<label for="instruct_separator_sequence">
<small data-i18n="Separator">Separator</small>
</label>
<div>
<textarea id="instruct_separator_sequence" class="text_pole wide100p textarea_compact autoSetHeight" maxlength="2000" placeholder="&mdash;" rows="1"></textarea>
</div>
</div>
</div>
</div>
</div>
@ -4335,8 +4444,10 @@
<div id="rm_print_characters_pagination">
<i id="charListGridToggle" class="fa-solid fa-table-cells-large menu_button" title="Toggle character grid view" data-i18n="[title]Toggle character grid view"></i>
<i id="bulkEditButton" class="fa-solid fa-edit menu_button bulkEditButton" title="Bulk edit characters" data-i18n="[title]Bulk edit characters"></i>
<i id="bulkDeleteButton" class="fa-solid fa-trash menu_button bulkDeleteButton" title="Bulk delete characters" data-i18n="[title]Bulk delete characters" style="display: none;"></i>
<i id="bulkEditButton" class="fa-solid fa-edit menu_button bulkEditButton" title="Bulk edit characters&#13;&#13;Click to toggle characters&#13;Shift + Click to select/deselect a range of characters&#13;Right-click for actions" data-i18n="[title]Bulk edit characters&#13;&#13;Click to toggle characters&#13;Shift + Click to select/deselect a range of characters&#13;Right-click for actions"></i>
<div id="bulkSelectedCount" class="bulkEditOptionElement paginationjs-nav"></div>
<i id="bulkSelectAllButton" class="fa-solid fa-check-double menu_button bulkEditOptionElement bulkSelectAllButton" title="Bulk select all characters" data-i18n="[title]Bulk select all characters" style="display: none;"></i>
<i id="bulkDeleteButton" class="fa-solid fa-trash menu_button bulkEditOptionElement bulkDeleteButton" title="Bulk delete characters" data-i18n="[title]Bulk delete characters" style="display: none;"></i>
</div>
<div id="rm_print_characters_block" class="flexFlowColumn"></div>
</div>
@ -4453,7 +4564,7 @@
Character's Note
</span>
</h4>
<textarea id="depth_prompt_prompt" name="depth_prompt_prompt" class="text_pole" rows="2" maxlength="50000" autocomplete="off" form="form_create" placeholder="(Text to be inserted in-chat @ designated depth)"></textarea>
<textarea id="depth_prompt_prompt" name="depth_prompt_prompt" class="text_pole" rows="5" maxlength="50000" autocomplete="off" form="form_create" placeholder="(Text to be inserted in-chat @ designated depth and role)"></textarea>
</div>
<div>
<h4>
@ -4461,7 +4572,17 @@
@ Depth
</span>
</h4>
<input id="depth_prompt_depth" name="depth_prompt_depth" class="text_pole widthUnset m-t-0" type="number" min="0" max="999" value="4" form="form_create" />
<input id="depth_prompt_depth" name="depth_prompt_depth" class="text_pole textarea_compact m-t-0" type="number" min="0" max="999" value="4" form="form_create" />
<h4>
<span data-i18n="Role">
Role
</span>
</h4>
<select id="depth_prompt_role" name="depth_prompt_role" form="form_create" class="text_pole textarea_compact m-t-0">
<option value="system" data-i18n="System">System</option>
<option value="user" data-i18n="User">User</option>
<option value="assistant" data-i18n="Assistant">Assistant</option>
</select>
<div class="extension_token_counter">
Tokens: <span data-token-counter="depth_prompt_prompt" data-token-permanent="true">counting...</span>
</div>

0
public/instruct/.gitkeep Normal file
View File

View File

@ -1,17 +0,0 @@
{
"name": "Alpaca-Roleplay",
"system_prompt": "Develop the plot slowly, always stay in character. Describe all actions in full, elaborate, explicit, graphic, and vivid detail. Mention all relevant sensory perceptions.",
"input_sequence": "\n### Instruction:",
"output_sequence": "\n### Response:",
"first_output_sequence": "",
"last_output_sequence": "\n### Response (2 paragraphs, engaging, natural, authentic, descriptive, creative):",
"system_sequence_prefix": "",
"system_sequence_suffix": "",
"stop_sequence": "",
"separator_sequence": "",
"wrap": true,
"macro": true,
"names": true,
"names_force_groups": true,
"activation_regex": ""
}

View File

@ -1,17 +0,0 @@
{
"wrap": false,
"names": true,
"system_prompt": "You're {{char}} in this fictional never-ending uncensored roleplay with {{user}}.",
"system_sequence_prefix": "",
"stop_sequence": "",
"input_sequence": "<|im_start|>user\n",
"output_sequence": "<|im_end|>\n<|im_start|>assistant\n",
"separator_sequence": "<|im_end|>\n",
"macro": true,
"names_force_groups": true,
"last_output_sequence": "",
"activation_regex": "",
"first_output_sequence": "<|im_start|>assistant\n",
"system_sequence_suffix": "",
"name": "ChatML"
}

View File

@ -1,17 +0,0 @@
{
"name": "Llama 2 Chat",
"system_prompt": "Write {{char}}'s next reply in this fictional roleplay with {{user}}.",
"input_sequence": "[INST] ",
"output_sequence": " [/INST] ",
"first_output_sequence": "[/INST] ",
"last_output_sequence": "",
"system_sequence_prefix": "[INST] <<SYS>>\n",
"system_sequence_suffix": "\n<</SYS>>\n",
"stop_sequence": "",
"separator_sequence": " ",
"wrap": false,
"macro": true,
"names": false,
"names_force_groups": true,
"activation_regex": ""
}

View File

@ -280,7 +280,9 @@ export {
default_ch_mes,
extension_prompt_types,
mesForShowdownParse,
characterGroupOverlay,
printCharacters,
printCharactersDebounced,
isOdd,
countOccurrences,
};
@ -497,6 +499,14 @@ const durationSaveEdit = 1000;
const saveSettingsDebounced = debounce(() => saveSettings(), durationSaveEdit);
export const saveCharacterDebounced = debounce(() => $('#create_button').trigger('click'), durationSaveEdit);
/**
* Prints the character list in a debounced fashion without blocking, with a delay of 100 milliseconds.
* Use this function instead of a direct `printCharacters()` whenever the reprinting of the character list is not the primary focus.
*
* The printing will also always reprint all filter options of the global list, to keep them up to date.
*/
const printCharactersDebounced = debounce(() => { printCharacters(false); }, 100);
/**
* @enum {string} System message types
*/
@ -752,6 +762,7 @@ function getCurrentChatId() {
const talkativeness_default = 0.5;
export const depth_prompt_depth_default = 4;
export const depth_prompt_role_default = 'system';
const per_page_default = 50;
var is_advanced_char_open = false;
@ -778,6 +789,7 @@ let create_save = {
alternate_greetings: [],
depth_prompt_prompt: '',
depth_prompt_depth: depth_prompt_depth_default,
depth_prompt_role: depth_prompt_role_default,
extensions: {},
};
@ -835,7 +847,7 @@ export let active_character = '';
/** The tag of the active group. (Coincidentally also the id) */
export let active_group = '';
export const entitiesFilter = new FilterHelper(debounce(printCharacters, 100));
export const entitiesFilter = new FilterHelper(printCharactersDebounced);
export const personasFilter = new FilterHelper(debounce(getUserAvatars, 100));
export function getRequestHeaders() {
@ -1274,19 +1286,31 @@ function getCharacterBlock(item, id) {
return template;
}
/**
* Prints the global character list, optionally doing a full refresh of the list
* Use this function whenever the reprinting of the character list is the primary focus, otherwise using `printCharactersDebounced` is preferred for a cleaner, non-blocking experience.
*
* The printing will also always reprint all filter options of the global list, to keep them up to date.
*
* @param {boolean} fullRefresh - If true, the list is fully refreshed and the navigation is being reset
*/
async function printCharacters(fullRefresh = false) {
if (fullRefresh) {
saveCharactersPage = 0;
printTagFilters(tag_filter_types.character);
printTagFilters(tag_filter_types.group_member);
await delay(1);
}
const storageKey = 'Characters_PerPage';
const listId = '#rm_print_characters_block';
const entities = getEntitiesList({ doFilter: true });
let currentScrollTop = $(listId).scrollTop();
if (fullRefresh) {
saveCharactersPage = 0;
currentScrollTop = 0;
await delay(1);
}
// We are actually always reprinting filters, as it "doesn't hurt", and this way they are always up to date
printTagFilters(tag_filter_types.character);
printTagFilters(tag_filter_types.group_member);
$('#rm_print_characters_pagination').pagination({
dataSource: entities,
pageSize: Number(localStorage.getItem(storageKey)) || per_page_default,
@ -1302,7 +1326,7 @@ async function printCharacters(fullRefresh = false) {
showNavigator: true,
callback: function (data) {
$(listId).empty();
if (isBogusFolderOpen()) {
if (power_user.bogus_folders && isBogusFolderOpen()) {
$(listId).append(getBackBlock());
}
if (!data.length) {
@ -1339,26 +1363,67 @@ async function printCharacters(fullRefresh = false) {
saveCharactersPage = e;
},
afterRender: function () {
$(listId).scrollTop(0);
$(listId).scrollTop(currentScrollTop);
},
});
favsToHotswap();
}
/** @typedef {object} Character - A character */
/** @typedef {object} Group - A group */
/**
* @typedef {object} Entity - Object representing a display entity
* @property {Character|Group|import('./scripts/tags.js').Tag|*} item - The item
* @property {string|number} id - The id
* @property {string} type - The type of this entity (character, group, tag)
* @property {Entity[]} [entities] - An optional list of entities relevant for this item
* @property {number} [hidden] - An optional number representing how many hidden entities this entity contains
*/
/**
* Converts the given character to its entity representation
*
* @param {Character} character - The character
* @param {string|number} id - The id of this character
* @returns {Entity} The entity for this character
*/
export function characterToEntity(character, id) {
return { item: character, id, type: 'character' };
}
/**
* Converts the given group to its entity representation
*
* @param {Group} group - The group
* @returns {Entity} The entity for this group
*/
export function groupToEntity(group) {
return { item: group, id: group.id, type: 'group' };
}
/**
* Converts the given tag to its entity representation
*
* @param {import('./scripts/tags.js').Tag} tag - The tag
* @returns {Entity} The entity for this tag
*/
export function tagToEntity(tag) {
return { item: structuredClone(tag), id: tag.id, type: 'tag', entities: [] };
}
/**
* Builds the full list of all entities available
*
* They will be correctly marked and filtered.
*
* @param {object} param0 - Optional parameters
* @param {boolean} [param0.doFilter] - Whether this entity list should already be filtered based on the global filters
* @param {boolean} [param0.doSort] - Whether the entity list should be sorted when returned
* @returns {Entity[]} All entities
*/
export function getEntitiesList({ doFilter = false, doSort = true } = {}) {
function characterToEntity(character, id) {
return { item: character, id, type: 'character' };
}
function groupToEntity(group) {
return { item: group, id: group.id, type: 'group' };
}
function tagToEntity(tag) {
return { item: structuredClone(tag), id: tag.id, type: 'tag', entities: [] };
}
let entities = [
...characters.map((item, index) => characterToEntity(item, index)),
...groups.map(item => groupToEntity(item)),
@ -2307,21 +2372,31 @@ function getStoppingStrings(isImpersonate, isContinue) {
* @param {boolean} skipWIAN whether to skip addition of World Info and Author's Note into the prompt
* @param {string} quietImage Image to use for the quiet prompt
* @param {string} quietName Name to use for the quiet prompt (defaults to "System:")
* @param {number} [responseLength] Maximum response length. If unset, the global default value is used.
* @returns
*/
export async function generateQuietPrompt(quiet_prompt, quietToLoud, skipWIAN, quietImage = null, quietName = null) {
export async function generateQuietPrompt(quiet_prompt, quietToLoud, skipWIAN, quietImage = null, quietName = null, responseLength = null) {
console.log('got into genQuietPrompt');
/** @type {GenerateOptions} */
const options = {
quiet_prompt,
quietToLoud,
skipWIAN: skipWIAN,
force_name2: true,
quietImage: quietImage,
quietName: quietName,
};
const generateFinished = await Generate('quiet', options);
return generateFinished;
const responseLengthCustomized = typeof responseLength === 'number' && responseLength > 0;
let originalResponseLength = -1;
try {
/** @type {GenerateOptions} */
const options = {
quiet_prompt,
quietToLoud,
skipWIAN: skipWIAN,
force_name2: true,
quietImage: quietImage,
quietName: quietName,
};
originalResponseLength = responseLengthCustomized ? saveResponseLength(main_api, responseLength) : -1;
const generateFinished = await Generate('quiet', options);
return generateFinished;
} finally {
if (responseLengthCustomized) {
restoreResponseLength(main_api, originalResponseLength);
}
}
}
/**
@ -2845,81 +2920,136 @@ class StreamingProcessor {
* @param {string} prompt Prompt to generate a message from
* @param {string} api API to use. Main API is used if not specified.
* @param {boolean} instructOverride true to override instruct mode, false to use the default value
* @param {boolean} quietToLoud true to generate a message in system mode, false to generate a message in character mode
* @param {string} [systemPrompt] System prompt to use. Only Instruct mode or OpenAI.
* @param {number} [responseLength] Maximum response length. If unset, the global default value is used.
* @returns {Promise<string>} Generated message
*/
export async function generateRaw(prompt, api, instructOverride) {
export async function generateRaw(prompt, api, instructOverride, quietToLoud, systemPrompt, responseLength) {
if (!api) {
api = main_api;
}
const abortController = new AbortController();
const isInstruct = power_user.instruct.enabled && main_api !== 'openai' && main_api !== 'novel' && !instructOverride;
const responseLengthCustomized = typeof responseLength === 'number' && responseLength > 0;
let originalResponseLength = -1;
const isInstruct = power_user.instruct.enabled && api !== 'openai' && api !== 'novel' && !instructOverride;
const isQuiet = true;
if (systemPrompt) {
systemPrompt = substituteParams(systemPrompt);
systemPrompt = isInstruct ? formatInstructModeSystemPrompt(systemPrompt) : systemPrompt;
prompt = api === 'openai' ? prompt : `${systemPrompt}\n${prompt}`;
}
prompt = substituteParams(prompt);
prompt = api == 'novel' ? adjustNovelInstructionPrompt(prompt) : prompt;
prompt = isInstruct ? formatInstructModeChat(name1, prompt, false, true, '', name1, name2, false) : prompt;
prompt = isInstruct ? (prompt + formatInstructModePrompt(name2, false, '', name1, name2)) : (prompt + '\n');
prompt = isInstruct ? (prompt + formatInstructModePrompt(name2, false, '', name1, name2, isQuiet, quietToLoud)) : (prompt + '\n');
let generateData = {};
try {
originalResponseLength = responseLengthCustomized ? saveResponseLength(api, responseLength) : -1;
let generateData = {};
switch (api) {
case 'kobold':
case 'koboldhorde':
if (preset_settings === 'gui') {
generateData = { prompt: prompt, gui_settings: true, max_length: amount_gen, max_context_length: max_context, api_server };
} else {
const isHorde = api === 'koboldhorde';
const koboldSettings = koboldai_settings[koboldai_setting_names[preset_settings]];
generateData = getKoboldGenerationData(prompt, koboldSettings, amount_gen, max_context, isHorde, 'quiet');
switch (api) {
case 'kobold':
case 'koboldhorde':
if (preset_settings === 'gui') {
generateData = { prompt: prompt, gui_settings: true, max_length: amount_gen, max_context_length: max_context, api_server };
} else {
const isHorde = api === 'koboldhorde';
const koboldSettings = koboldai_settings[koboldai_setting_names[preset_settings]];
generateData = getKoboldGenerationData(prompt, koboldSettings, amount_gen, max_context, isHorde, 'quiet');
}
break;
case 'novel': {
const novelSettings = novelai_settings[novelai_setting_names[nai_settings.preset_settings_novel]];
generateData = getNovelGenerationData(prompt, novelSettings, amount_gen, false, false, null, 'quiet');
break;
}
break;
case 'novel': {
const novelSettings = novelai_settings[novelai_setting_names[nai_settings.preset_settings_novel]];
generateData = getNovelGenerationData(prompt, novelSettings, amount_gen, false, false, null, 'quiet');
break;
case 'textgenerationwebui':
generateData = getTextGenGenerationData(prompt, amount_gen, false, false, null, 'quiet');
break;
case 'openai': {
generateData = [{ role: 'user', content: prompt.trim() }];
if (systemPrompt) {
generateData.unshift({ role: 'system', content: systemPrompt.trim() });
}
} break;
}
let data = {};
if (api == 'koboldhorde') {
data = await generateHorde(prompt, generateData, abortController.signal, false);
} else if (api == 'openai') {
data = await sendOpenAIRequest('quiet', generateData, abortController.signal);
} else {
const generateUrl = getGenerateUrl(api);
const response = await fetch(generateUrl, {
method: 'POST',
headers: getRequestHeaders(),
cache: 'no-cache',
body: JSON.stringify(generateData),
signal: abortController.signal,
});
if (!response.ok) {
const error = await response.json();
throw error;
}
data = await response.json();
}
if (data.error) {
throw new Error(data.error);
}
const message = cleanUpMessage(extractMessageFromData(data), false, false, true);
if (!message) {
throw new Error('No message generated');
}
return message;
} finally {
if (responseLengthCustomized) {
restoreResponseLength(api, originalResponseLength);
}
case 'textgenerationwebui':
generateData = getTextGenGenerationData(prompt, amount_gen, false, false, null, 'quiet');
break;
case 'openai':
generateData = [{ role: 'user', content: prompt.trim() }];
}
}
let data = {};
if (api == 'koboldhorde') {
data = await generateHorde(prompt, generateData, abortController.signal, false);
} else if (api == 'openai') {
data = await sendOpenAIRequest('quiet', generateData, abortController.signal);
/**
* Temporarily change the response length for the specified API.
* @param {string} api API to use.
* @param {number} responseLength Target response length.
* @returns {number} The original response length.
*/
function saveResponseLength(api, responseLength) {
let oldValue = -1;
if (api === 'openai') {
oldValue = oai_settings.openai_max_tokens;
oai_settings.openai_max_tokens = responseLength;
} else {
const generateUrl = getGenerateUrl(api);
const response = await fetch(generateUrl, {
method: 'POST',
headers: getRequestHeaders(),
cache: 'no-cache',
body: JSON.stringify(generateData),
signal: abortController.signal,
});
if (!response.ok) {
const error = await response.json();
throw error;
}
data = await response.json();
oldValue = max_context;
max_context = responseLength;
}
return oldValue;
}
if (data.error) {
throw new Error(data.error);
/**
* Restore the original response length for the specified API.
* @param {string} api API to use.
* @param {number} responseLength Target response length.
* @returns {void}
*/
function restoreResponseLength(api, responseLength) {
if (api === 'openai') {
oai_settings.openai_max_tokens = responseLength;
} else {
max_context = responseLength;
}
const message = cleanUpMessage(extractMessageFromData(data), false, false, true);
if (!message) {
throw new Error('No message generated');
}
return message;
}
/**
@ -3112,12 +3242,14 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu
if (selected_group && Array.isArray(groupDepthPrompts) && groupDepthPrompts.length > 0) {
groupDepthPrompts.forEach((value, index) => {
setExtensionPrompt('DEPTH_PROMPT_' + index, value.text, extension_prompt_types.IN_CHAT, value.depth, extension_settings.note.allowWIScan);
const role = getExtensionPromptRoleByName(value.role);
setExtensionPrompt('DEPTH_PROMPT_' + index, value.text, extension_prompt_types.IN_CHAT, value.depth, extension_settings.note.allowWIScan, role);
});
} else {
const depthPromptText = baseChatReplace(characters[this_chid].data?.extensions?.depth_prompt?.prompt?.trim(), name1, name2) || '';
const depthPromptDepth = characters[this_chid].data?.extensions?.depth_prompt?.depth ?? depth_prompt_depth_default;
setExtensionPrompt('DEPTH_PROMPT', depthPromptText, extension_prompt_types.IN_CHAT, depthPromptDepth, extension_settings.note.allowWIScan);
const depthPromptRole = getExtensionPromptRoleByName(characters[this_chid].data?.extensions?.depth_prompt?.role ?? depth_prompt_role_default);
setExtensionPrompt('DEPTH_PROMPT', depthPromptText, extension_prompt_types.IN_CHAT, depthPromptDepth, extension_settings.note.allowWIScan, depthPromptRole);
}
// Parse example messages
@ -3128,10 +3260,6 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu
mesExamples = '';
}
const mesExamplesRaw = mesExamples;
if (mesExamples && isInstruct) {
mesExamples = formatInstructModeExamples(mesExamples, name1, name2);
}
/**
* Adds a block heading to the examples string.
* @param {string} examplesStr
@ -3139,13 +3267,17 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu
*/
function addBlockHeading(examplesStr) {
const exampleSeparator = power_user.context.example_separator ? `${substituteParams(power_user.context.example_separator)}\n` : '';
const blockHeading = main_api === 'openai' ? '<START>\n' : exampleSeparator;
const blockHeading = main_api === 'openai' ? '<START>\n' : (exampleSeparator || (isInstruct ? '<START>\n' : ''));
return examplesStr.split(/<START>/gi).slice(1).map(block => `${blockHeading}${block.trim()}\n`);
}
let mesExamplesArray = addBlockHeading(mesExamples);
let mesExamplesRawArray = addBlockHeading(mesExamplesRaw);
if (mesExamplesArray && isInstruct) {
mesExamplesArray = formatInstructModeExamples(mesExamplesArray, name1, name2);
}
// First message in fresh 1-on-1 chat reacts to user/character settings changes
if (chat.length) {
chat[0].mes = substituteParams(chat[0].mes);
@ -3259,6 +3391,8 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu
let chat2 = [];
let continue_mag = '';
const userMessageIndices = [];
for (let i = coreChat.length - 1, j = 0; i >= 0; i--, j++) {
if (main_api == 'openai') {
chat2[i] = coreChat[j].mes;
@ -3286,6 +3420,22 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu
chat2[i] = chat2[i].slice(0, chat2[i].lastIndexOf(coreChat[j].mes) + coreChat[j].mes.length);
continue_mag = coreChat[j].mes;
}
if (coreChat[j].is_user) {
userMessageIndices.push(i);
}
}
let addUserAlignment = isInstruct && power_user.instruct.user_alignment_message;
let userAlignmentMessage = '';
if (addUserAlignment) {
const alignmentMessage = {
name: name1,
mes: power_user.instruct.user_alignment_message,
is_user: true,
};
userAlignmentMessage = formatMessageHistoryItem(alignmentMessage, isInstruct, false);
}
// Add persona description to prompt
@ -3344,6 +3494,7 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu
allAnchors,
quiet_prompt,
cyclePrompt,
userAlignmentMessage,
].join('').replace(/\r/gm, '');
return getTokenCount(encodeString, power_user.token_padding);
}
@ -3360,18 +3511,24 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu
}
// Collect enough messages to fill the context
let arrMes = [];
let arrMes = new Array(chat2.length);
let tokenCount = getMessagesTokenCount();
for (let item of chat2) {
// not needed for OAI prompting
if (main_api == 'openai') {
break;
let lastAddedIndex = -1;
// Pre-allocate all injections first.
// If it doesn't fit - user shot himself in the foot
for (const index of injectedIndices) {
const item = chat2[index];
if (typeof item !== 'string') {
continue;
}
tokenCount += getTokenCount(item.replace(/\r/gm, ''));
chatString = item + chatString;
if (tokenCount < this_max_context) {
arrMes[arrMes.length] = item;
arrMes[index] = item;
lastAddedIndex = Math.max(lastAddedIndex, index);
} else {
break;
}
@ -3380,8 +3537,62 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu
await delay(1);
}
for (let i = 0; i < chat2.length; i++) {
// not needed for OAI prompting
if (main_api == 'openai') {
break;
}
// Skip already injected messages
if (arrMes[i] !== undefined) {
continue;
}
const item = chat2[i];
if (typeof item !== 'string') {
continue;
}
tokenCount += getTokenCount(item.replace(/\r/gm, ''));
chatString = item + chatString;
if (tokenCount < this_max_context) {
arrMes[i] = item;
lastAddedIndex = Math.max(lastAddedIndex, i);
} else {
break;
}
// Prevent UI thread lock on tokenization
await delay(1);
}
// Add user alignment message if last message is not a user message
const stoppedAtUser = userMessageIndices.includes(lastAddedIndex);
if (addUserAlignment && !stoppedAtUser) {
tokenCount += getTokenCount(userAlignmentMessage.replace(/\r/gm, ''));
chatString = userAlignmentMessage + chatString;
arrMes.push(userAlignmentMessage);
injectedIndices.push(arrMes.length - 1);
}
// Unsparse the array. Adjust injected indices
const newArrMes = [];
const newInjectedIndices = [];
for (let i = 0; i < arrMes.length; i++) {
if (arrMes[i] !== undefined) {
newArrMes.push(arrMes[i]);
if (injectedIndices.includes(i)) {
newInjectedIndices.push(newArrMes.length - 1);
}
}
}
arrMes = newArrMes;
injectedIndices = newInjectedIndices;
if (main_api !== 'openai') {
setInContextMessages(arrMes.length, type);
setInContextMessages(arrMes.length - injectedIndices.length, type);
}
// Estimate how many unpinned example messages fit in the context
@ -3424,15 +3635,19 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu
console.debug('generating prompt');
chatString = '';
arrMes = arrMes.reverse();
arrMes.forEach(function (item, i, arr) {// For added anchors and others
arrMes.forEach(function (item, i, arr) {
// OAI doesn't need all of this
if (main_api === 'openai') {
return;
}
// Cohee: I'm not even sure what this is for anymore
// Cohee: This removes a newline from the end of the last message in the context
// Last prompt line will add a newline if it's not a continuation
// In instruct mode it only removes it if wrap is enabled and it's not a quiet generation
if (i === arrMes.length - 1 && type !== 'continue') {
item = item.replace(/\n?$/, '');
if (!isInstruct || (power_user.instruct.wrap && type !== 'quiet')) {
item = item.replace(/\n?$/, '');
}
}
mesSend[mesSend.length] = { message: item, extensionPrompts: [] };
@ -3471,7 +3686,7 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu
//TODO: respect output_sequence vs last_output_sequence settings
//TODO: decide how to prompt this to clarify who is talking 'Narrator', 'System', etc.
if (isInstruct) {
lastMesString += '\n' + quietAppend; // + power_user.instruct.output_sequence + '\n';
lastMesString += quietAppend; // + power_user.instruct.output_sequence + '\n';
} else {
lastMesString += quietAppend;
}
@ -3492,7 +3707,8 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu
// Get instruct mode line
if (isInstruct && !isContinue) {
const name = (quiet_prompt && !quietToLoud) ? (quietName ?? 'System') : (isImpersonate ? name1 : name2);
lastMesString += formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2);
const isQuiet = quiet_prompt && type == 'quiet';
lastMesString += formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2, isQuiet, quietToLoud);
}
// Get non-instruct impersonation line
@ -3659,7 +3875,7 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu
};
finalMesSend.forEach((item, i) => {
item.injected = Array.isArray(injectedIndices) && injectedIndices.includes(i);
item.injected = injectedIndices.includes(finalMesSend.length - i - 1);
});
let data = {
@ -4027,10 +4243,6 @@ function doChatInject(messages, isContinue) {
}
}
for (let i = 0; i < injectedIndices.length; i++) {
injectedIndices[i] = messages.length - injectedIndices[i] - 1;
}
messages.reverse();
return injectedIndices;
}
@ -4230,10 +4442,19 @@ export async function sendMessageAsUser(messageText, messageBias, insertAt = nul
}
}
export function getMaxContextSize() {
/**
* Gets the maximum usable context size for the current API.
* @param {number|null} overrideResponseLength Optional override for the response length.
* @returns {number} Maximum usable context size.
*/
export function getMaxContextSize(overrideResponseLength = null) {
if (typeof overrideResponseLength !== 'number' || overrideResponseLength <= 0 || isNaN(overrideResponseLength)) {
overrideResponseLength = null;
}
let this_max_context = 1487;
if (main_api == 'kobold' || main_api == 'koboldhorde' || main_api == 'textgenerationwebui') {
this_max_context = (max_context - amount_gen);
this_max_context = (max_context - (overrideResponseLength || amount_gen));
}
if (main_api == 'novel') {
this_max_context = Number(max_context);
@ -4250,10 +4471,10 @@ export function getMaxContextSize() {
}
}
this_max_context = this_max_context - amount_gen;
this_max_context = this_max_context - (overrideResponseLength || amount_gen);
}
if (main_api == 'openai') {
this_max_context = oai_settings.openai_max_context - oai_settings.openai_max_tokens;
this_max_context = oai_settings.openai_max_context - (overrideResponseLength || oai_settings.openai_max_tokens);
}
return this_max_context;
}
@ -4615,7 +4836,7 @@ function extractMessageFromData(data) {
case 'novel':
return data.output;
case 'openai':
return data?.choices?.[0]?.message?.content ?? data?.choices?.[0]?.text ?? '';
return data?.choices?.[0]?.message?.content ?? data?.choices?.[0]?.text ?? data?.text ?? '';
default:
return '';
}
@ -5356,7 +5577,7 @@ function buildAvatarList(block, entities, { templateId = 'inline_avatar_template
avatarTemplate.attr('data-type', entity.type);
avatarTemplate.attr({ 'chid': id, 'id': `CharID${id}` });
avatarTemplate.find('img').attr('src', this_avatar).attr('alt', entity.item.name);
avatarTemplate.attr('title', `[Character] ${entity.item.name}`);
avatarTemplate.attr('title', `[Character] ${entity.item.name}\nFile: ${entity.item.avatar}`);
if (highlightFavs) {
avatarTemplate.toggleClass('is_fav', entity.item.fav || entity.item.fav == 'true');
avatarTemplate.find('.ch_fav').val(entity.item.fav);
@ -6615,6 +6836,7 @@ export function select_selected_character(chid) {
$('#scenario_pole').val(characters[chid].scenario);
$('#depth_prompt_prompt').val(characters[chid].data?.extensions?.depth_prompt?.prompt ?? '');
$('#depth_prompt_depth').val(characters[chid].data?.extensions?.depth_prompt?.depth ?? depth_prompt_depth_default);
$('#depth_prompt_role').val(characters[chid].data?.extensions?.depth_prompt?.role ?? depth_prompt_role_default);
$('#talkativeness_slider').val(characters[chid].talkativeness || talkativeness_default);
$('#mes_example_textarea').val(characters[chid].mes_example);
$('#selected_chat_pole').val(characters[chid].chat);
@ -6685,6 +6907,7 @@ function select_rm_create() {
$('#scenario_pole').val(create_save.scenario);
$('#depth_prompt_prompt').val(create_save.depth_prompt_prompt);
$('#depth_prompt_depth').val(create_save.depth_prompt_depth);
$('#depth_prompt_role').val(create_save.depth_prompt_role);
$('#mes_example_textarea').val(create_save.mes_example);
$('#character_json_data').val('');
$('#avatar_div').css('display', 'flex');
@ -6728,6 +6951,30 @@ export function setExtensionPrompt(key, value, position, depth, scan = false, ro
};
}
/**
* Gets a enum value of the extension prompt role by its name.
* @param {string} roleName The name of the extension prompt role.
* @returns {number} The role id of the extension prompt.
*/
export function getExtensionPromptRoleByName(roleName) {
// If the role is already a valid number, return it
if (typeof roleName === 'number' && Object.values(extension_prompt_roles).includes(roleName)) {
return roleName;
}
switch (roleName) {
case 'system':
return extension_prompt_roles.SYSTEM;
case 'user':
return extension_prompt_roles.USER;
case 'assistant':
return extension_prompt_roles.ASSISTANT;
}
// Skill issue?
return extension_prompt_roles.SYSTEM;
}
/**
* Removes all char A/N prompt injections from the chat.
* To clean up when switching from groups to solo and vice versa.
@ -6788,18 +7035,19 @@ function onScenarioOverrideRemoveClick() {
* @param {string} type
* @param {string} inputValue - Value to set the input to.
* @param {PopupOptions} options - Options for the popup.
* @typedef {{okButton?: string, rows?: number, wide?: boolean, large?: boolean }} PopupOptions - Options for the popup.
* @typedef {{okButton?: string, rows?: number, wide?: boolean, large?: boolean, allowHorizontalScrolling?: boolean, allowVerticalScrolling?: boolean }} PopupOptions - Options for the popup.
* @returns
*/
function callPopup(text, type, inputValue = '', { okButton, rows, wide, large } = {}) {
function callPopup(text, type, inputValue = '', { okButton, rows, wide, large, allowHorizontalScrolling, allowVerticalScrolling } = {}) {
dialogueCloseStop = true;
if (type) {
popup_type = type;
}
$('#dialogue_popup').toggleClass('wide_dialogue_popup', !!wide);
$('#dialogue_popup').toggleClass('large_dialogue_popup', !!large);
$('#dialogue_popup').toggleClass('horizontal_scrolling_dialogue_popup', !!allowHorizontalScrolling);
$('#dialogue_popup').toggleClass('vertical_scrolling_dialogue_popup', !!allowVerticalScrolling);
$('#dialogue_popup_cancel').css('display', 'inline-block');
switch (popup_type) {
@ -7343,6 +7591,7 @@ async function createOrEditCharacter(e) {
{ id: '#scenario_pole', callback: value => create_save.scenario = value },
{ id: '#depth_prompt_prompt', callback: value => create_save.depth_prompt_prompt = value },
{ id: '#depth_prompt_depth', callback: value => create_save.depth_prompt_depth = value, defaultValue: depth_prompt_depth_default },
{ id: '#depth_prompt_role', callback: value => create_save.depth_prompt_role = value, defaultValue: depth_prompt_role_default },
{ id: '#mes_example_textarea', callback: value => create_save.mes_example = value },
{ id: '#character_json_data', callback: () => { } },
{ id: '#alternate_greetings_template', callback: value => create_save.alternate_greetings = value, defaultValue: [] },
@ -7938,6 +8187,11 @@ const CONNECT_API_MAP = {
button: '#api_button_openai',
source: chat_completion_sources.CUSTOM,
},
'cohere': {
selected: 'cohere',
button: '#api_button_openai',
source: chat_completion_sources.COHERE,
},
'infermaticai': {
selected: 'textgenerationwebui',
button: '#api_button_textgenerationwebui',
@ -8286,7 +8540,7 @@ function addDebugFunctions() {
registerDebugFunction('generationTest', 'Send a generation request', 'Generates text using the currently selected API.', async () => {
const text = prompt('Input text:', 'Hello');
toastr.info('Working on it...');
const message = await generateRaw(text, null, '');
const message = await generateRaw(text, null, false, false);
alert(message);
});
@ -8338,7 +8592,7 @@ jQuery(async function () {
$('#groupCurrentMemberListToggle .inline-drawer-icon').trigger('click');
}, 200);
$('#chat').on('mousewheel touchstart', () => {
$('#chat').on('wheel touchstart', () => {
scrollLock = true;
});
@ -8712,6 +8966,7 @@ jQuery(async function () {
'#talkativeness_slider': function () { create_save.talkativeness = Number($('#talkativeness_slider').val()); },
'#depth_prompt_prompt': function () { create_save.depth_prompt_prompt = String($('#depth_prompt_prompt').val()); },
'#depth_prompt_depth': function () { create_save.depth_prompt_depth = Number($('#depth_prompt_depth').val()); },
'#depth_prompt_role': function () { create_save.depth_prompt_role = String($('#depth_prompt_role').val()); },
};
Object.keys(elementsToUpdate).forEach(function (id) {
@ -8877,6 +9132,7 @@ jQuery(async function () {
{ id: 'api_key_dreamgen', secret: SECRET_KEYS.DREAMGEN },
{ id: 'api_key_openrouter-tg', secret: SECRET_KEYS.OPENROUTER },
{ id: 'api_key_koboldcpp', secret: SECRET_KEYS.KOBOLDCPP },
{ id: 'api_key_llamacpp', secret: SECRET_KEYS.LLAMACPP },
];
for (const key of keys) {

View File

@ -1,6 +1,7 @@
'use strict';
import {
characterGroupOverlay,
callPopup,
characters,
deleteCharacter,
@ -9,25 +10,15 @@ import {
getCharacters,
getPastCharacterChats,
getRequestHeaders,
printCharacters,
buildAvatarList,
characterToEntity,
printCharactersDebounced,
} from '../script.js';
import { favsToHotswap } from './RossAscends-mods.js';
import { hideLoader, showLoader } from './loader.js';
import { convertCharacterToPersona } from './personas.js';
import { createTagInput, getTagKeyForEntity, tag_map } from './tags.js';
// Utility object for popup messages.
const popupMessage = {
deleteChat(characterCount) {
return `<h3>Delete ${characterCount} characters?</h3>
<b>THIS IS PERMANENT!<br><br>
<label for="del_char_checkbox" class="checkbox_label justifyCenter">
<input type="checkbox" id="del_char_checkbox" />
<span>Also delete the chat files</span>
</label><br></b>`;
},
};
import { createTagInput, getTagKeyForEntity, getTagsList, printTagList, tag_map, compareTagsForSort, removeTagFromMap } from './tags.js';
/**
* Static object representing the actions of the
@ -38,16 +29,16 @@ class CharacterContextMenu {
* Tag one or more characters,
* opens a popup.
*
* @param selectedCharacters
* @param {Array<number>} selectedCharacters
*/
static tag = (selectedCharacters) => {
BulkTagPopupHandler.show(selectedCharacters);
characterGroupOverlay.bulkTagPopupHandler.show(selectedCharacters);
};
/**
* Duplicate one or more characters
*
* @param characterId
* @param {number} characterId
* @returns {Promise<any>}
*/
static duplicate = async (characterId) => {
@ -72,7 +63,7 @@ class CharacterContextMenu {
* Favorite a character
* and highlight it.
*
* @param characterId
* @param {number} characterId
* @returns {Promise<void>}
*/
static favorite = async (characterId) => {
@ -108,7 +99,7 @@ class CharacterContextMenu {
* Convert one or more characters to persona,
* may open a popup for one or more characters.
*
* @param characterId
* @param {number} characterId
* @returns {Promise<void>}
*/
static persona = async (characterId) => await convertCharacterToPersona(characterId);
@ -117,8 +108,8 @@ class CharacterContextMenu {
* Delete one or more characters,
* opens a popup.
*
* @param characterId
* @param deleteChats
* @param {number} characterId
* @param {boolean} [deleteChats]
* @returns {Promise<void>}
*/
static delete = async (characterId, deleteChats = false) => {
@ -196,13 +187,39 @@ class CharacterContextMenu {
* Represents a tag control not bound to a single character
*/
class BulkTagPopupHandler {
static #getHtml = (characterIds) => {
const characterData = JSON.stringify({ characterIds: characterIds });
/**
* The characters for this popup
* @type {number[]}
*/
characterIds;
/**
* A storage of the current mutual tags, as calculated by getMutualTags()
* @type {object[]}
*/
currentMutualTags;
/**
* Sets up the bulk popup menu handler for the given overlay.
*
* Characters can be passed in with the show() call.
*/
constructor() { }
/**
* Gets the HTML as a string that is going to be the popup for the bulk tag edit
*
* @returns String containing the html for the popup
*/
#getHtml = () => {
const characterData = JSON.stringify({ characterIds: this.characterIds });
return `<div id="bulk_tag_shadow_popup">
<div id="bulk_tag_popup">
<div id="bulk_tag_popup_holder">
<h3 class="m-b-1">Add tags to ${characterIds.length} characters</h3>
<br>
<h3 class="marginBot5">Modify tags of ${this.characterIds.length} characters</h3>
<small class="bulk_tags_desc m-b-1">Add or remove the mutual tags of all selected characters.</small>
<div id="bulk_tags_avatars_block" class="avatars_inline avatars_inline_small tags tags_inline"></div>
<br>
<div id="bulk_tags_div" class="marginBot5" data-characters='${characterData}'>
<div class="tag_controls">
<input id="bulkTagInput" class="text_pole tag_input wide100p margin0" data-i18n="[placeholder]Search / Create Tags" placeholder="Search / Create tags" maxlength="25" />
@ -211,51 +228,117 @@ class BulkTagPopupHandler {
<div id="bulkTagList" class="m-t-1 tags"></div>
</div>
<div id="dialogue_popup_controls" class="m-t-1">
<div id="bulk_tag_popup_reset" class="menu_button" title="Remove all tags from the selected characters" data-i18n="[title]Remove all tags from the selected characters">
<i class="fa-solid fa-trash-can margin-right-10px"></i>
All
</div>
<div id="bulk_tag_popup_remove_mutual" class="menu_button" title="Remove all mutual tags from the selected characters" data-i18n="[title]Remove all mutual tags from the selected characters">
<i class="fa-solid fa-trash-can margin-right-10px"></i>
Mutual
</div>
<div id="bulk_tag_popup_cancel" class="menu_button" data-i18n="Cancel">Close</div>
<div id="bulk_tag_popup_reset" class="menu_button" data-i18n="Cancel">Remove all</div>
</div>
</div>
</div>
</div>
`;
</div>`;
};
/**
* Append and show the tag control
*
* @param characters - The characters assigned to this control
* @param {number[]} characterIds - The characters that are shown inside the popup
*/
static show(characters) {
document.body.insertAdjacentHTML('beforeend', this.#getHtml(characters));
createTagInput('#bulkTagInput', '#bulkTagList');
show(characterIds) {
// shallow copy character ids persistently into this tooltip
this.characterIds = characterIds.slice();
if (this.characterIds.length == 0) {
console.log('No characters selected for bulk edit tags.');
return;
}
document.body.insertAdjacentHTML('beforeend', this.#getHtml());
const entities = this.characterIds.map(id => characterToEntity(characters[id], id)).filter(entity => entity.item !== undefined);
buildAvatarList($('#bulk_tags_avatars_block'), entities);
// Print the tag list with all mutuable tags, marking them as removable. That is the initial fill
printTagList($('#bulkTagList'), { tags: () => this.getMutualTags(), tagOptions: { removable: true } });
// Tag input with resolvable list for the mutual tags to get redrawn, so that newly added tags get sorted correctly
createTagInput('#bulkTagInput', '#bulkTagList', { tags: () => this.getMutualTags(), tagOptions: { removable: true }});
document.querySelector('#bulk_tag_popup_reset').addEventListener('click', this.resetTags.bind(this));
document.querySelector('#bulk_tag_popup_remove_mutual').addEventListener('click', this.removeMutual.bind(this));
document.querySelector('#bulk_tag_popup_cancel').addEventListener('click', this.hide.bind(this));
document.querySelector('#bulk_tag_popup_reset').addEventListener('click', this.resetTags.bind(this, characters));
}
/**
* Builds a list of all tags that the provided characters have in common.
*
* @returns {Array<object>} A list of mutual tags
*/
getMutualTags() {
if (this.characterIds.length == 0) {
return [];
}
if (this.characterIds.length === 1) {
// Just use tags of the single character
return getTagsList(getTagKeyForEntity(this.characterIds[0]));
}
// Find mutual tags for multiple characters
const allTags = this.characterIds.map(cid => getTagsList(getTagKeyForEntity(cid)));
const mutualTags = allTags.reduce((mutual, characterTags) =>
mutual.filter(tag => characterTags.some(cTag => cTag.id === tag.id))
);
this.currentMutualTags = mutualTags.sort(compareTagsForSort);
return this.currentMutualTags;
}
/**
* Hide and remove the tag control
*/
static hide() {
hide() {
let popupElement = document.querySelector('#bulk_tag_shadow_popup');
if (popupElement) {
document.body.removeChild(popupElement);
}
printCharacters(true);
// No need to redraw here, all tags actions were redrawn when they happened
}
/**
* Empty the tag map for the given characters
*
* @param characterIds
*/
static resetTags(characterIds) {
characterIds.forEach((characterId) => {
resetTags() {
for (const characterId of this.characterIds) {
const key = getTagKeyForEntity(characterId);
if (key) tag_map[key] = [];
});
}
printCharacters(true);
$('#bulkTagList').empty();
printCharactersDebounced();
}
/**
* Remove the mutual tags for all given characters
*/
removeMutual() {
const mutualTags = this.getMutualTags();
for (const characterId of this.characterIds) {
for(const tag of mutualTags) {
removeTagFromMap(tag.id, characterId);
}
}
$('#bulkTagList').empty();
printCharactersDebounced();
}
}
@ -290,6 +373,7 @@ class BulkEditOverlay {
static selectModeClass = 'group_overlay_mode_select';
static selectedClass = 'character_selected';
static legacySelectedClass = 'bulk_select_checkbox';
static bulkSelectedCountId = 'bulkSelectedCount';
static longPressDelay = 2500;
@ -297,6 +381,18 @@ class BulkEditOverlay {
#longPress = false;
#stateChangeCallbacks = [];
#selectedCharacters = [];
#bulkTagPopupHandler = new BulkTagPopupHandler();
/**
* @typedef {object} LastSelected - An object noting the last selected character and its state.
* @property {string} [characterId] - The character id of the last selected character.
* @property {boolean} [select] - The selected state of the last selected character. <c>true</c> if it was selected, <c>false</c> if it was deselected.
*/
/**
* @type {LastSelected} - An object noting the last selected character and its state.
*/
lastSelected = { characterId: undefined, select: undefined };
/**
* Locks other pointer actions when the context menu is open
@ -345,12 +441,21 @@ class BulkEditOverlay {
/**
*
* @returns {*[]}
* @returns {number[]}
*/
get selectedCharacters() {
return this.#selectedCharacters;
}
/**
* The instance of the bulk tag popup handler that handles tagging of all selected characters
*
* @returns {BulkTagPopupHandler}
*/
get bulkTagPopupHandler() {
return this.#bulkTagPopupHandler;
}
constructor() {
if (bulkEditOverlayInstance instanceof BulkEditOverlay)
return bulkEditOverlayInstance;
@ -533,27 +638,110 @@ class BulkEditOverlay {
event.stopPropagation();
const character = event.currentTarget;
const characterId = character.getAttribute('chid');
const alreadySelected = this.selectedCharacters.includes(characterId);
if (!this.#contextMenuOpen && !this.#cancelNextToggle) {
if (event.shiftKey) {
// Shift click might have selected text that we don't want to. Unselect it.
document.getSelection().removeAllRanges();
const legacyBulkEditCheckbox = character.querySelector('.' + BulkEditOverlay.legacySelectedClass);
// Only toggle when context menu is closed and wasn't just closed.
if (!this.#contextMenuOpen && !this.#cancelNextToggle)
if (alreadySelected) {
character.classList.remove(BulkEditOverlay.selectedClass);
if (legacyBulkEditCheckbox) legacyBulkEditCheckbox.checked = false;
this.dismissCharacter(characterId);
this.handleShiftClick(character);
} else {
character.classList.add(BulkEditOverlay.selectedClass);
if (legacyBulkEditCheckbox) legacyBulkEditCheckbox.checked = true;
this.selectCharacter(characterId);
this.toggleSingleCharacter(character);
}
}
this.#cancelNextToggle = false;
};
/**
* When shift click was held down, this function handles the multi select of characters in a single click.
*
* If the last clicked character was deselected, and the current one was deselected too, it will deselect all currently selected characters between those two.
* If the last clicked character was selected, and the current one was selected too, it will select all currently not selected characters between those two.
* If the states do not match, nothing will happen.
*
* @param {HTMLElement} currentCharacter - The html element of the currently toggled character
*/
handleShiftClick = (currentCharacter) => {
const characterId = currentCharacter.getAttribute('chid');
const select = !this.selectedCharacters.includes(characterId);
if (this.lastSelected.characterId && this.lastSelected.select !== undefined) {
// Only if select state and the last select state match we execute the range select
if (select === this.lastSelected.select) {
this.toggleCharactersInRange(currentCharacter, select);
}
}
};
/**
* Toggles the selection of a given characters
*
* @param {HTMLElement} character - The html element of a character
* @param {object} param1 - Optional params
* @param {boolean} [param1.markState] - Whether the toggle of this character should be remembered as the last done toggle
*/
toggleSingleCharacter = (character, { markState = true } = {}) => {
const characterId = character.getAttribute('chid');
const select = !this.selectedCharacters.includes(characterId);
const legacyBulkEditCheckbox = character.querySelector('.' + BulkEditOverlay.legacySelectedClass);
if (select) {
character.classList.add(BulkEditOverlay.selectedClass);
if (legacyBulkEditCheckbox) legacyBulkEditCheckbox.checked = true;
this.#selectedCharacters.push(String(characterId));
} else {
character.classList.remove(BulkEditOverlay.selectedClass);
if (legacyBulkEditCheckbox) legacyBulkEditCheckbox.checked = false;
this.#selectedCharacters = this.#selectedCharacters.filter(item => String(characterId) !== item)
}
this.updateSelectedCount();
if (markState) {
this.lastSelected.characterId = characterId;
this.lastSelected.select = select;
}
};
/**
* Updates the selected count element with the current count
*
* @param {number} [countOverride] - optional override for a manual number to set
*/
updateSelectedCount = (countOverride = undefined) => {
const count = countOverride ?? this.selectedCharacters.length;
$(`#${BulkEditOverlay.bulkSelectedCountId}`).text(count).attr('title', `${count} characters selected`);
};
/**
* Toggles the selection of characters in a given range.
* The range is provided by the given character and the last selected one remembered in the selection state.
*
* @param {HTMLElement} currentCharacter - The html element of the currently toggled character
* @param {boolean} select - <c>true</c> if the characters in the range are to be selected, <c>false</c> if deselected
*/
toggleCharactersInRange = (currentCharacter, select) => {
const currentCharacterId = currentCharacter.getAttribute('chid');
const characters = Array.from(document.querySelectorAll('#' + BulkEditOverlay.containerId + ' .' + BulkEditOverlay.characterClass));
const startIndex = characters.findIndex(c => c.getAttribute('chid') === this.lastSelected.characterId);
const endIndex = characters.findIndex(c => c.getAttribute('chid') === currentCharacterId);
for (let i = Math.min(startIndex, endIndex); i <= Math.max(startIndex, endIndex); i++) {
const character = characters[i];
const characterId = character.getAttribute('chid');
const isCharacterSelected = this.selectedCharacters.includes(characterId);
// Only toggle the character if it wasn't on the state we have are toggling towards.
// Also doing a weird type check, because typescript checker doesn't like the return of 'querySelectorAll'.
if ((select && !isCharacterSelected || !select && isCharacterSelected) && character instanceof HTMLElement) {
this.toggleSingleCharacter(character, { markState: currentCharacterId == characterId });
}
}
};
handleContextMenuShow = (event) => {
event.preventDefault();
CharacterContextMenu.show(...this.#getContextMenuPosition(event));
@ -607,6 +795,29 @@ class BulkEditOverlay {
this.browseState();
};
/**
* Gets the HTML as a string that is displayed inside the popup for the bulk delete
*
* @param {Array<number>} characterIds - The characters that are shown inside the popup
* @returns String containing the html for the popup content
*/
static #getDeletePopupContentHtml = (characterIds) => {
return `
<h3 class="marginBot5">Delete ${characterIds.length} characters?</h3>
<span class="bulk_delete_note">
<i class="fa-solid fa-triangle-exclamation warning margin-r5"></i>
<b>THIS IS PERMANENT!</b>
</span>
<div id="bulk_delete_avatars_block" class="avatars_inline avatars_inline_small tags tags_inline m-t-1"></div>
<br>
<div id="bulk_delete_options" class="m-b-1">
<label for="del_char_checkbox" class="checkbox_label justifyCenter">
<input type="checkbox" id="del_char_checkbox" />
<span>Also delete the chat files</span>
</label>
</div>`;
}
/**
* Request user input before concurrently handle deletion
* requests.
@ -614,8 +825,9 @@ class BulkEditOverlay {
* @returns {Promise<number>}
*/
handleContextMenuDelete = () => {
callPopup(
popupMessage.deleteChat(this.selectedCharacters.length), null)
const characterIds = this.selectedCharacters;
const popupContent = BulkEditOverlay.#getDeletePopupContentHtml(characterIds);
const promise = callPopup(popupContent, null)
.then((accept) => {
if (true !== accept) return;
@ -623,11 +835,17 @@ class BulkEditOverlay {
showLoader();
toastr.info('We\'re deleting your characters, please wait...', 'Working on it');
Promise.allSettled(this.selectedCharacters.map(async characterId => CharacterContextMenu.delete(characterId, deleteChats)))
return Promise.allSettled(characterIds.map(async characterId => CharacterContextMenu.delete(characterId, deleteChats)))
.then(() => getCharacters())
.then(() => this.browseState())
.finally(() => hideLoader());
});
// At this moment the popup is already changed in the dom, but not yet closed/resolved. We build the avatar list here
const entities = characterIds.map(id => characterToEntity(characters[id], id)).filter(entity => entity.item !== undefined);
buildAvatarList($('#bulk_delete_avatars_block'), entities);
return promise;
};
/**
@ -635,14 +853,11 @@ class BulkEditOverlay {
*/
handleContextMenuTag = () => {
CharacterContextMenu.tag(this.selectedCharacters);
this.browseState();
};
addStateChangeCallback = callback => this.stateChangeCallbacks.push(callback);
selectCharacter = characterId => this.selectedCharacters.push(String(characterId));
dismissCharacter = characterId => this.#selectedCharacters = this.selectedCharacters.filter(item => String(characterId) !== item);
/**
* Clears internal character storage and
* removes visual highlight.

View File

@ -27,7 +27,7 @@ import {
import { LoadLocal, SaveLocal, LoadLocalBool } from './f-localStorage.js';
import { selected_group, is_group_generating, openGroupById } from './group-chats.js';
import { getTagKeyForEntity } from './tags.js';
import { getTagKeyForEntity, applyTagsOnCharacterSelect } from './tags.js';
import {
SECRET_KEYS,
secret_state,
@ -252,6 +252,10 @@ async function RA_autoloadchat() {
const active_character_id = characters.findIndex(x => getTagKeyForEntity(x) === active_character);
if (active_character_id !== null) {
await selectCharacterById(String(active_character_id));
// Do a little tomfoolery to spoof the tag selector
const selectedCharElement = $(`#rm_print_characters_block .character_select[chid="${active_character_id}"]`)
applyTagsOnCharacterSelect.call(selectedCharElement);
}
}
@ -346,6 +350,7 @@ function RA_autoconnect(PrevApi) {
|| (secret_state[SECRET_KEYS.AI21] && oai_settings.chat_completion_source == chat_completion_sources.AI21)
|| (secret_state[SECRET_KEYS.MAKERSUITE] && oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE)
|| (secret_state[SECRET_KEYS.MISTRALAI] && oai_settings.chat_completion_source == chat_completion_sources.MISTRALAI)
|| (secret_state[SECRET_KEYS.COHERE] && oai_settings.chat_completion_source == chat_completion_sources.COHERE)
|| (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)
) {
$('#api_button_openai').trigger('click');

View File

@ -1,4 +1,4 @@
import { characters, getCharacters, handleDeleteCharacter, callPopup } from '../script.js';
import { characters, getCharacters, handleDeleteCharacter, callPopup, characterGroupOverlay } from '../script.js';
import { BulkEditOverlay, BulkEditOverlayState } from './BulkEditOverlay.js';
@ -6,18 +6,20 @@ let is_bulk_edit = false;
const enableBulkEdit = () => {
enableBulkSelect();
(new BulkEditOverlay()).selectState();
// show the delete button
$('#bulkDeleteButton').show();
characterGroupOverlay.selectState();
// show the bulk edit option buttons
$('.bulkEditOptionElement').show();
is_bulk_edit = true;
characterGroupOverlay.updateSelectedCount(0);
};
const disableBulkEdit = () => {
disableBulkSelect();
(new BulkEditOverlay()).browseState();
// hide the delete button
$('#bulkDeleteButton').hide();
characterGroupOverlay.browseState();
// hide the bulk edit option buttons
$('.bulkEditOptionElement').hide();
is_bulk_edit = false;
characterGroupOverlay.updateSelectedCount(0);
};
const toggleBulkEditMode = (isBulkEdit) => {
@ -28,7 +30,7 @@ const toggleBulkEditMode = (isBulkEdit) => {
}
};
(new BulkEditOverlay()).addStateChangeCallback((state) => {
characterGroupOverlay.addStateChangeCallback((state) => {
if (state === BulkEditOverlayState.select) enableBulkEdit();
if (state === BulkEditOverlayState.browse) disableBulkEdit();
});
@ -41,6 +43,32 @@ function onEditButtonClick() {
toggleBulkEditMode(is_bulk_edit);
}
/**
* Toggles the select state of all characters in bulk edit mode to selected. If all are selected, they'll be deselected.
*/
function onSelectAllButtonClick() {
console.log('Bulk select all button clicked');
const characters = Array.from(document.querySelectorAll('#' + BulkEditOverlay.containerId + ' .' + BulkEditOverlay.characterClass));
let atLeastOneSelected = false;
for (const character of characters) {
const checked = $(character).find('.bulk_select_checkbox:checked').length > 0;
if (!checked && character instanceof HTMLElement) {
characterGroupOverlay.toggleSingleCharacter(character);
atLeastOneSelected = true;
}
}
if (!atLeastOneSelected) {
// If none was selected, trigger click on all to deselect all of them
for(const character of characters) {
const checked = $(character).find('.bulk_select_checkbox:checked') ?? false;
if (checked && character instanceof HTMLElement) {
characterGroupOverlay.toggleSingleCharacter(character);
}
}
}
}
/**
* Deletes the character with the given chid.
*
@ -56,32 +84,8 @@ async function deleteCharacter(this_chid) {
async function onDeleteButtonClick() {
console.log('Delete button clicked');
// Create a mapping of chid to avatar
let toDelete = [];
$('.bulk_select_checkbox:checked').each((i, el) => {
const chid = $(el).parent().attr('chid');
const avatar = characters[chid].avatar;
// Add the avatar to the list of avatars to delete
toDelete.push(avatar);
});
const confirm = await callPopup('<h3>Are you sure you want to delete these characters?</h3>You would need to delete the chat files manually.<br>', 'confirm');
if (!confirm) {
console.log('User cancelled delete');
return;
}
// Delete the characters
for (const avatar of toDelete) {
console.log(`Deleting character with avatar ${avatar}`);
await getCharacters();
//chid should be the key of the character with the given avatar
const chid = Object.keys(characters).find((key) => characters[key].avatar === avatar);
console.log(`Deleting character with chid ${chid}`);
await deleteCharacter(chid);
}
// We just let the button trigger the context menu delete option
await characterGroupOverlay.handleContextMenuDelete();
}
/**
@ -89,6 +93,10 @@ async function onDeleteButtonClick() {
*/
function enableBulkSelect() {
$('#rm_print_characters_block .character_select').each((i, el) => {
// Prevent checkbox from adding multiple times (because of stage change callback)
if ($(el).find('.bulk_select_checkbox').length > 0) {
return;
}
const checkbox = $('<input type=\'checkbox\' class=\'bulk_select_checkbox\'>');
checkbox.on('change', () => {
// Do something when the checkbox is changed
@ -115,5 +123,6 @@ function disableBulkSelect() {
*/
jQuery(() => {
$('#bulkEditButton').on('click', onEditButtonClick);
$('#bulkSelectAllButton').on('click', onSelectAllButtonClick);
$('#bulkDeleteButton').on('click', onDeleteButtonClick);
});

View File

@ -1,11 +1,25 @@
import { getStringHash, debounce, waitUntilCondition, extractAllWords } from '../../utils.js';
import { getContext, getApiUrl, extension_settings, doExtrasFetch, modules } from '../../extensions.js';
import { animation_duration, eventSource, event_types, extension_prompt_roles, extension_prompt_types, generateQuietPrompt, is_send_press, saveSettingsDebounced, substituteParams } from '../../../script.js';
import { getStringHash, debounce, waitUntilCondition, extractAllWords, delay } from '../../utils.js';
import { getContext, getApiUrl, extension_settings, doExtrasFetch, modules, renderExtensionTemplate } from '../../extensions.js';
import {
activateSendButtons,
deactivateSendButtons,
animation_duration,
eventSource,
event_types,
extension_prompt_roles,
extension_prompt_types,
generateQuietPrompt,
is_send_press,
saveSettingsDebounced,
substituteParams,
generateRaw,
getMaxContextSize,
} from '../../../script.js';
import { is_group_generating, selected_group } from '../../group-chats.js';
import { registerSlashCommand } from '../../slash-commands.js';
import { loadMovingUIState } from '../../power-user.js';
import { dragElement } from '../../RossAscends-mods.js';
import { getTextTokens, tokenizers } from '../../tokenizers.js';
import { getTextTokens, getTokenCount, tokenizers } from '../../tokenizers.js';
export { MODULE_NAME };
const MODULE_NAME = '1_memory';
@ -39,7 +53,13 @@ const summary_sources = {
'main': 'main',
};
const defaultPrompt = '[Pause your roleplay. Summarize the most important facts and events that have happened in the chat so far. If a summary already exists in your memory, use that as a base and expand with new facts. Limit the summary to {{words}} words or less. Your response should include nothing but the summary.]';
const prompt_builders = {
DEFAULT: 0,
RAW_BLOCKING: 1,
RAW_NON_BLOCKING: 2,
};
const defaultPrompt = '[Pause your roleplay. Summarize the most important facts and events in the story so far. If a summary already exists in your memory, use that as a base and expand with new facts. Limit the summary to {{words}} words or less. Your response should include nothing but the summary.]';
const defaultTemplate = '[Summary: {{summary}}]';
const defaultSettings = {
@ -57,12 +77,21 @@ const defaultSettings = {
promptWordsStep: 25,
promptInterval: 10,
promptMinInterval: 0,
promptMaxInterval: 100,
promptMaxInterval: 250,
promptIntervalStep: 1,
promptForceWords: 0,
promptForceWordsStep: 100,
promptMinForceWords: 0,
promptMaxForceWords: 10000,
overrideResponseLength: 0,
overrideResponseLengthMin: 0,
overrideResponseLengthMax: 4096,
overrideResponseLengthStep: 16,
maxMessagesPerRequest: 0,
maxMessagesPerRequestMin: 0,
maxMessagesPerRequestMax: 250,
maxMessagesPerRequestStep: 1,
prompt_builder: prompt_builders.RAW_BLOCKING,
};
function loadSettings() {
@ -87,9 +116,88 @@ function loadSettings() {
$('#memory_role').val(extension_settings.memory.role).trigger('input');
$(`input[name="memory_position"][value="${extension_settings.memory.position}"]`).prop('checked', true).trigger('input');
$('#memory_prompt_words_force').val(extension_settings.memory.promptForceWords).trigger('input');
$(`input[name="memory_prompt_builder"][value="${extension_settings.memory.prompt_builder}"]`).prop('checked', true).trigger('input');
$('#memory_override_response_length').val(extension_settings.memory.overrideResponseLength).trigger('input');
$('#memory_max_messages_per_request').val(extension_settings.memory.maxMessagesPerRequest).trigger('input');
switchSourceControls(extension_settings.memory.source);
}
async function onPromptForceWordsAutoClick() {
const context = getContext();
const maxPromptLength = getMaxContextSize(extension_settings.memory.overrideResponseLength);
const chat = context.chat;
const allMessages = chat.filter(m => !m.is_system && m.mes).map(m => m.mes);
const messagesWordCount = allMessages.map(m => extractAllWords(m)).flat().length;
const averageMessageWordCount = messagesWordCount / allMessages.length;
const tokensPerWord = getTokenCount(allMessages.join('\n')) / messagesWordCount;
const wordsPerToken = 1 / tokensPerWord;
const maxPromptLengthWords = Math.round(maxPromptLength * wordsPerToken);
// How many words should pass so that messages will start be dropped out of context;
const wordsPerPrompt = Math.floor(maxPromptLength / tokensPerWord);
// How many words will be needed to fit the allowance buffer
const summaryPromptWords = extractAllWords(extension_settings.memory.prompt).length;
const promptAllowanceWords = maxPromptLengthWords - extension_settings.memory.promptWords - summaryPromptWords;
const averageMessagesPerPrompt = Math.floor(promptAllowanceWords / averageMessageWordCount);
const maxMessagesPerSummary = extension_settings.memory.maxMessagesPerRequest || 0;
const targetMessagesInPrompt = maxMessagesPerSummary > 0 ? maxMessagesPerSummary : Math.max(0, averageMessagesPerPrompt);
const targetSummaryWords = (targetMessagesInPrompt * averageMessageWordCount) + (promptAllowanceWords / 4);
console.table({
maxPromptLength,
maxPromptLengthWords,
promptAllowanceWords,
averageMessagesPerPrompt,
targetMessagesInPrompt,
targetSummaryWords,
wordsPerPrompt,
wordsPerToken,
tokensPerWord,
messagesWordCount,
});
const ROUNDING = 100;
extension_settings.memory.promptForceWords = Math.max(1, Math.floor(targetSummaryWords / ROUNDING) * ROUNDING);
$('#memory_prompt_words_force').val(extension_settings.memory.promptForceWords).trigger('input');
}
async function onPromptIntervalAutoClick() {
const context = getContext();
const maxPromptLength = getMaxContextSize(extension_settings.memory.overrideResponseLength);
const chat = context.chat;
const allMessages = chat.filter(m => !m.is_system && m.mes).map(m => m.mes);
const messagesWordCount = allMessages.map(m => extractAllWords(m)).flat().length;
const messagesTokenCount = getTokenCount(allMessages.join('\n'));
const tokensPerWord = messagesTokenCount / messagesWordCount;
const averageMessageTokenCount = messagesTokenCount / allMessages.length;
const targetSummaryTokens = Math.round(extension_settings.memory.promptWords * tokensPerWord);
const promptTokens = getTokenCount(extension_settings.memory.prompt);
const promptAllowance = maxPromptLength - promptTokens - targetSummaryTokens;
const maxMessagesPerSummary = extension_settings.memory.maxMessagesPerRequest || 0;
const averageMessagesPerPrompt = Math.floor(promptAllowance / averageMessageTokenCount);
const targetMessagesInPrompt = maxMessagesPerSummary > 0 ? maxMessagesPerSummary : Math.max(0, averageMessagesPerPrompt);
const adjustedAverageMessagesPerPrompt = targetMessagesInPrompt + (averageMessagesPerPrompt - targetMessagesInPrompt) / 4;
console.table({
maxPromptLength,
promptAllowance,
targetSummaryTokens,
promptTokens,
messagesWordCount,
messagesTokenCount,
tokensPerWord,
averageMessageTokenCount,
averageMessagesPerPrompt,
targetMessagesInPrompt,
adjustedAverageMessagesPerPrompt,
maxMessagesPerSummary,
});
const ROUNDING = 5;
extension_settings.memory.promptInterval = Math.max(1, Math.floor(adjustedAverageMessagesPerPrompt / ROUNDING) * ROUNDING);
$('#memory_prompt_interval').val(extension_settings.memory.promptInterval).trigger('input');
}
function onSummarySourceChange(event) {
const value = event.target.value;
extension_settings.memory.source = value;
@ -98,8 +206,8 @@ function onSummarySourceChange(event) {
}
function switchSourceControls(value) {
$('#memory_settings [data-source]').each((_, element) => {
const source = $(element).data('source');
$('#memory_settings [data-summary-source]').each((_, element) => {
const source = $(element).data('summary-source');
$(element).toggle(source === value);
});
}
@ -130,6 +238,10 @@ function onMemoryPromptIntervalInput() {
saveSettingsDebounced();
}
function onMemoryPromptRestoreClick() {
$('#memory_prompt').val(defaultPrompt).trigger('input');
}
function onMemoryPromptInput() {
const value = $(this).val();
extension_settings.memory.prompt = value;
@ -171,6 +283,20 @@ function onMemoryPromptWordsForceInput() {
saveSettingsDebounced();
}
function onOverrideResponseLengthInput() {
const value = $(this).val();
extension_settings.memory.overrideResponseLength = Number(value);
$('#memory_override_response_length_value').text(extension_settings.memory.overrideResponseLength);
saveSettingsDebounced();
}
function onMaxMessagesPerRequestInput() {
const value = $(this).val();
extension_settings.memory.maxMessagesPerRequest = Number(value);
$('#memory_max_messages_per_request_value').text(extension_settings.memory.maxMessagesPerRequest);
saveSettingsDebounced();
}
function saveLastValues() {
const context = getContext();
lastGroupId = context.groupId;
@ -196,6 +322,22 @@ function getLatestMemoryFromChat(chat) {
return '';
}
function getIndexOfLatestChatSummary(chat) {
if (!Array.isArray(chat) || !chat.length) {
return -1;
}
const reversedChat = chat.slice().reverse();
reversedChat.shift();
for (let mes of reversedChat) {
if (mes.extra && mes.extra.memory) {
return chat.indexOf(mes);
}
}
return -1;
}
async function onChatEvent() {
// Module not enabled
if (extension_settings.memory.source === summary_sources.extras) {
@ -359,8 +501,41 @@ async function summarizeChatMain(context, force, skipWIAN) {
console.debug('Summarization prompt is empty. Skipping summarization.');
return;
}
console.log('sending summary prompt');
const summary = await generateQuietPrompt(prompt, false, skipWIAN);
let summary = '';
let index = null;
if (prompt_builders.DEFAULT === extension_settings.memory.prompt_builder) {
summary = await generateQuietPrompt(prompt, false, skipWIAN, '', '', extension_settings.memory.overrideResponseLength);
}
if ([prompt_builders.RAW_BLOCKING, prompt_builders.RAW_NON_BLOCKING].includes(extension_settings.memory.prompt_builder)) {
const lock = extension_settings.memory.prompt_builder === prompt_builders.RAW_BLOCKING;
try {
if (lock) {
deactivateSendButtons();
}
const { rawPrompt, lastUsedIndex } = await getRawSummaryPrompt(context, prompt);
if (lastUsedIndex === null || lastUsedIndex === -1) {
if (force) {
toastr.info('To try again, remove the latest summary.', 'No messages found to summarize');
}
return null;
}
summary = await generateRaw(rawPrompt, '', false, false, prompt, extension_settings.memory.overrideResponseLength);
index = lastUsedIndex;
} finally {
if (lock) {
activateSendButtons();
}
}
}
const newContext = getContext();
// something changed during summarization request
@ -371,10 +546,83 @@ async function summarizeChatMain(context, force, skipWIAN) {
return;
}
setMemoryContext(summary, true);
setMemoryContext(summary, true, index);
return summary;
}
/**
* Get the raw summarization prompt from the chat context.
* @param {object} context ST context
* @param {string} prompt Summarization system prompt
* @returns {Promise<{rawPrompt: string, lastUsedIndex: number}>} Raw summarization prompt
*/
async function getRawSummaryPrompt(context, prompt) {
/**
* Get the memory string from the chat buffer.
* @param {boolean} includeSystem Include prompt into the memory string
* @returns {string} Memory string
*/
function getMemoryString(includeSystem) {
const delimiter = '\n\n';
const stringBuilder = [];
const bufferString = chatBuffer.slice().join(delimiter);
if (includeSystem) {
stringBuilder.push(prompt);
}
if (latestSummary) {
stringBuilder.push(latestSummary);
}
stringBuilder.push(bufferString);
return stringBuilder.join(delimiter).trim();
}
const chat = context.chat.slice();
const latestSummary = getLatestMemoryFromChat(chat);
const latestSummaryIndex = getIndexOfLatestChatSummary(chat);
chat.pop(); // We always exclude the last message from the buffer
const chatBuffer = [];
const PADDING = 64;
const PROMPT_SIZE = getMaxContextSize(extension_settings.memory.overrideResponseLength);
let latestUsedMessage = null;
for (let index = latestSummaryIndex + 1; index < chat.length; index++) {
const message = chat[index];
if (!message) {
break;
}
if (message.is_system || !message.mes) {
continue;
}
const entry = `${message.name}:\n${message.mes}`;
chatBuffer.push(entry);
const tokens = getTokenCount(getMemoryString(true), PADDING);
await delay(1);
if (tokens > PROMPT_SIZE) {
chatBuffer.pop();
break;
}
latestUsedMessage = message;
if (extension_settings.memory.maxMessagesPerRequest > 0 && chatBuffer.length >= extension_settings.memory.maxMessagesPerRequest) {
break;
}
}
const lastUsedIndex = context.chat.indexOf(latestUsedMessage);
const rawPrompt = getMemoryString(false);
return { rawPrompt, lastUsedIndex };
}
async function summarizeChatExtras(context) {
function getMemoryString() {
return (longMemory + '\n\n' + memoryBuffer.slice().reverse().join('\n\n')).trim();
@ -482,12 +730,24 @@ function onMemoryContentInput() {
setMemoryContext(value, true);
}
function onMemoryPromptBuilderInput(e) {
const value = Number(e.target.value);
extension_settings.memory.prompt_builder = value;
saveSettingsDebounced();
}
function reinsertMemory() {
const existingValue = $('#memory_contents').val();
const existingValue = String($('#memory_contents').val());
setMemoryContext(existingValue, false);
}
function setMemoryContext(value, saveToMessage) {
/**
* Set the summary value to the context and save it to the chat message extra.
* @param {string} value Value of a summary
* @param {boolean} saveToMessage Should the summary be saved to the chat message extra
* @param {number|null} index Index of the chat message to save the summary to. If null, the pre-last message is used.
*/
function setMemoryContext(value, saveToMessage, index = null) {
const context = getContext();
context.setExtensionPrompt(MODULE_NAME, formatMemoryValue(value), extension_settings.memory.position, extension_settings.memory.depth, false, extension_settings.memory.role);
$('#memory_contents').val(value);
@ -497,7 +757,7 @@ function setMemoryContext(value, saveToMessage) {
console.debug('Role: ' + extension_settings.memory.role);
if (saveToMessage && context.chat.length) {
const idx = context.chat.length - 2;
const idx = index ?? context.chat.length - 2;
const mes = context.chat[idx < 0 ? 0 : idx];
if (!mes.extra) {
@ -573,6 +833,14 @@ function setupListeners() {
$('#memory_role').off('click').on('input', onMemoryRoleInput);
$('input[name="memory_position"]').off('click').on('change', onMemoryPositionChange);
$('#memory_prompt_words_force').off('click').on('input', onMemoryPromptWordsForceInput);
$('#memory_prompt_builder_default').off('click').on('input', onMemoryPromptBuilderInput);
$('#memory_prompt_builder_raw_blocking').off('click').on('input', onMemoryPromptBuilderInput);
$('#memory_prompt_builder_raw_non_blocking').off('click').on('input', onMemoryPromptBuilderInput);
$('#memory_prompt_restore').off('click').on('click', onMemoryPromptRestoreClick);
$('#memory_prompt_interval_auto').off('click').on('click', onPromptIntervalAutoClick);
$('#memory_prompt_words_auto').off('click').on('click', onPromptForceWordsAutoClick);
$('#memory_override_response_length').off('click').on('input', onOverrideResponseLengthInput);
$('#memory_max_messages_per_request').off('click').on('input', onMaxMessagesPerRequestInput);
$('#summarySettingsBlockToggle').off('click').on('click', function () {
console.log('saw settings button click');
$('#summarySettingsBlock').slideToggle(200, 'swing'); //toggleClass("hidden");
@ -581,91 +849,7 @@ function setupListeners() {
jQuery(function () {
function addExtensionControls() {
const settingsHtml = `
<div id="memory_settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<div class="flex-container alignitemscenter margin0"><b>Summarize</b><i id="summaryExtensionPopoutButton" class="fa-solid fa-window-restore menu_button margin0"></i></div>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<div id="summaryExtensionDrawerContents">
<label for="summary_source">Summarize with:</label>
<select id="summary_source">
<option value="main">Main API</option>
<option value="extras">Extras API</option>
</select><br>
<div class="flex-container justifyspacebetween alignitemscenter">
<span class="flex1">Current summary:</span>
<div id="memory_restore" class="menu_button flex1 margin0"><span>Restore Previous</span></div>
</div>
<textarea id="memory_contents" class="text_pole textarea_compact" rows="6" placeholder="Summary will be generated here..."></textarea>
<div class="memory_contents_controls">
<div id="memory_force_summarize" data-source="main" class="menu_button menu_button_icon" title="Trigger a summary update right now." data-i18n="Trigger a summary update right now.">
<i class="fa-solid fa-database"></i>
<span>Summarize now</span>
</div>
<label for="memory_frozen" title="Disable automatic summary updates. While paused, the summary remains as-is. You can still force an update by pressing the Summarize now button (which is only available with the Main API)." data-i18n="[title]Disable automatic summary updates. While paused, the summary remains as-is. You can still force an update by pressing the Summarize now button (which is only available with the Main API)."><input id="memory_frozen" type="checkbox" />Pause</label>
<label for="memory_skipWIAN" title="Omit World Info and Author's Note from text to be summarized. Only has an effect when using the Main API. The Extras API always omits WI/AN." data-i18n="[title]Omit World Info and Author's Note from text to be summarized. Only has an effect when using the Main API. The Extras API always omits WI/AN."><input id="memory_skipWIAN" type="checkbox" />No WI/AN</label>
</div>
<div class="memory_contents_controls">
<div id="summarySettingsBlockToggle" class="menu_button menu_button_icon" title="Edit summarization prompt, insertion position, etc.">
<i class="fa-solid fa-cog"></i>
<span>Summary Settings</span>
</div>
</div>
<div id="summarySettingsBlock" style="display:none;">
<div class="memory_template">
<label for="memory_template">Insertion Template</label>
<textarea id="memory_template" class="text_pole textarea_compact" rows="2" placeholder="{{summary}} will resolve to the current summary contents."></textarea>
</div>
<label for="memory_position">Injection Position</label>
<div class="radio_group">
<label>
<input type="radio" name="memory_position" value="2" />
Before Main Prompt / Story String
</label>
<label>
<input type="radio" name="memory_position" value="0" />
After Main Prompt / Story String
</label>
<label class="flex-container alignItemsCenter" title="How many messages before the current end of the chat." data-i18n="[title]How many messages before the current end of the chat.">
<input type="radio" name="memory_position" value="1" />
In-chat @ Depth <input id="memory_depth" class="text_pole widthUnset" type="number" min="0" max="999" />
as
<select id="memory_role" class="text_pole widthNatural">
<option value="0">System</option>
<option value="1">User</option>
<option value="2">Assistant</option>
</select>
</label>
</div>
<div data-source="main" class="memory_contents_controls">
</div>
<div data-source="main">
<label for="memory_prompt" class="title_restorable">
Summary Prompt
</label>
<textarea id="memory_prompt" class="text_pole textarea_compact" rows="6" placeholder="This prompt will be sent to AI to request the summary generation. {{words}} will resolve to the 'Number of words' parameter."></textarea>
<label for="memory_prompt_words">Summary length (<span id="memory_prompt_words_value"></span> words)</label>
<input id="memory_prompt_words" type="range" value="${defaultSettings.promptWords}" min="${defaultSettings.promptMinWords}" max="${defaultSettings.promptMaxWords}" step="${defaultSettings.promptWordsStep}" />
<label for="memory_prompt_interval">Update every <span id="memory_prompt_interval_value"></span> messages</label>
<small>0 = disable</small>
<input id="memory_prompt_interval" type="range" value="${defaultSettings.promptInterval}" min="${defaultSettings.promptMinInterval}" max="${defaultSettings.promptMaxInterval}" step="${defaultSettings.promptIntervalStep}" />
<label for="memory_prompt_words_force">Update every <span id="memory_prompt_words_force_value"></span> words</label>
<small>0 = disable</small>
<input id="memory_prompt_words_force" type="range" value="${defaultSettings.promptForceWords}" min="${defaultSettings.promptMinForceWords}" max="${defaultSettings.promptMaxForceWords}" step="${defaultSettings.promptForceWordsStep}" />
<small>If both sliders are non-zero, then both will trigger summary updates a their respective intervals.</small>
</div>
</div>
</div>
</div>
</div>
</div>
`;
const settingsHtml = renderExtensionTemplate('memory', 'settings', { defaultSettings });
$('#extensions_settings2').append(settingsHtml);
setupListeners();
$('#summaryExtensionPopoutButton').off('click').on('click', function (e) {

View File

@ -0,0 +1,136 @@
<div id="memory_settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<div class="flex-container alignitemscenter margin0">
<b>Summarize</b>
<i id="summaryExtensionPopoutButton" class="fa-solid fa-window-restore menu_button margin0"></i>
</div>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<div id="summaryExtensionDrawerContents">
<label for="summary_source">Summarize with:</label>
<select id="summary_source">
<option value="main">Main API</option>
<option value="extras">Extras API</option>
</select><br>
<div class="flex-container justifyspacebetween alignitemscenter">
<span class="flex1">Current summary:</span>
<div id="memory_restore" class="menu_button flex1 margin0">
<span>Restore Previous</span>
</div>
</div>
<textarea id="memory_contents" class="text_pole textarea_compact" rows="6" placeholder="Summary will be generated here..."></textarea>
<div class="memory_contents_controls">
<div id="memory_force_summarize" data-summary-source="main" class="menu_button menu_button_icon" title="Trigger a summary update right now." data-i18n="Trigger a summary update right now.">
<i class="fa-solid fa-database"></i>
<span>Summarize now</span>
</div>
<label for="memory_frozen" title="Disable automatic summary updates. While paused, the summary remains as-is. You can still force an update by pressing the Summarize now button (which is only available with the Main API)." data-i18n="[title]Disable automatic summary updates. While paused, the summary remains as-is. You can still force an update by pressing the Summarize now button (which is only available with the Main API)."><input id="memory_frozen" type="checkbox" />Pause</label>
<label data-summary-source="main" for="memory_skipWIAN" title="Omit World Info and Author's Note from text to be summarized. Only has an effect when using the Main API. The Extras API always omits WI/AN." data-i18n="[title]Omit World Info and Author's Note from text to be summarized. Only has an effect when using the Main API. The Extras API always omits WI/AN.">
<input id="memory_skipWIAN" type="checkbox" />
<span>No WI/AN</span>
</label>
</div>
<div class="memory_contents_controls">
<div id="summarySettingsBlockToggle" class="menu_button menu_button_icon" title="Edit summarization prompt, insertion position, etc.">
<i class="fa-solid fa-cog"></i>
<span>Summary Settings</span>
</div>
</div>
<div id="summarySettingsBlock" style="display:none;">
<div data-summary-source="main">
<label>
Prompt builder
</label>
<label class="checkbox_label" for="memory_prompt_builder_raw_blocking" title="Extension will build its own prompt using messages that were not summarized yet. Blocks the chat until the summary is generated.">
<input id="memory_prompt_builder_raw_blocking" type="radio" name="memory_prompt_builder" value="1" />
<span>Raw, blocking</span>
</label>
<label class="checkbox_label" for="memory_prompt_builder_raw_non_blocking" title="Extension will build its own prompt using messages that were not summarized yet. Does not block the chat while the summary is being generated. Not all backends support this mode.">
<input id="memory_prompt_builder_raw_non_blocking" type="radio" name="memory_prompt_builder" value="2" />
<span>Raw, non-blocking</span>
</label>
<label class="checkbox_label" id="memory_prompt_builder_default" title="Extension will use the regular main prompt builder and add the summary request to it as the last system message.">
<input id="memory_prompt_builder_default" type="radio" name="memory_prompt_builder" value="0" />
<span>Classic, blocking</span>
</label>
</div>
<div data-summary-source="main">
<label for="memory_prompt" class="title_restorable">
<span data-i18n="Summary Prompt">Summary Prompt</span>
<div id="memory_prompt_restore" title="Restore default prompt" class="right_menu_button">
<div class="fa-solid fa-clock-rotate-left"></div>
</div>
</label>
<textarea id="memory_prompt" class="text_pole textarea_compact" rows="6" placeholder="This prompt will be sent to AI to request the summary generation. &lcub;&lcub;words&rcub;&rcub; will resolve to the 'Number of words' parameter."></textarea>
<label for="memory_prompt_words">Target summary length (<span id="memory_prompt_words_value"></span> words)</label>
<input id="memory_prompt_words" type="range" value="{{defaultSettings.promptWords}}" min="{{defaultSettings.promptMinWords}}" max="{{defaultSettings.promptMaxWords}}" step="{{defaultSettings.promptWordsStep}}" />
<label for="memory_override_response_length">
API response length (<span id="memory_override_response_length_value"></span> tokens)
<small class="memory_disabled_hint">0 = default</small>
</label>
<input id="memory_override_response_length" type="range" value="{{defaultSettings.overrideResponseLength}}" min="{{defaultSettings.overrideResponseLengthMin}}" max="{{defaultSettings.overrideResponseLengthMax}}" step="{{defaultSettings.overrideResponseLengthStep}}" />
<label for="memory_max_messages_per_request">
[Raw] Max messages per request (<span id="memory_max_messages_per_request_value"></span>)
<small class="memory_disabled_hint">0 = unlimited</small>
</label>
<input id="memory_max_messages_per_request" type="range" value="{{defaultSettings.maxMessagesPerRequest}}" min="{{defaultSettings.maxMessagesPerRequestMin}}" max="{{defaultSettings.maxMessagesPerRequestMax}}" step="{{defaultSettings.maxMessagesPerRequestStep}}" />
<h4 data-i18n="Update frequency" class="textAlignCenter">
Update frequency
</h4>
<label for="memory_prompt_interval" class="title_restorable">
<span>
Update every <span id="memory_prompt_interval_value"></span> messages
<small class="memory_disabled_hint">0 = disable</small>
</span>
<div id="memory_prompt_interval_auto" title="Try to automatically adjust the interval based on the chat metrics." class="right_menu_button">
<div class="fa-solid fa-wand-magic-sparkles"></div>
</div>
</label>
<input id="memory_prompt_interval" type="range" value="{{defaultSettings.promptInterval}}" min="{{defaultSettings.promptMinInterval}}" max="{{defaultSettings.promptMaxInterval}}" step="{{defaultSettings.promptIntervalStep}}" />
<label for="memory_prompt_words_force" class="title_restorable">
<span>
Update every <span id="memory_prompt_words_force_value"></span> words
<small class="memory_disabled_hint">0 = disable</small>
</span>
<div id="memory_prompt_words_auto" title="Try to automatically adjust the interval based on the chat metrics." class="right_menu_button">
<div class="fa-solid fa-wand-magic-sparkles"></div>
</div>
</label>
<input id="memory_prompt_words_force" type="range" value="{{defaultSettings.promptForceWords}}" min="{{defaultSettings.promptMinForceWords}}" max="{{defaultSettings.promptMaxForceWords}}" step="{{defaultSettings.promptForceWordsStep}}" />
<small>If both sliders are non-zero, then both will trigger summary updates at their respective intervals.</small>
<hr>
</div>
<div class="memory_template">
<label for="memory_template">Injection Template</label>
<textarea id="memory_template" class="text_pole textarea_compact" rows="2" placeholder="&lcub;&lcub;summary&rcub;&rcub; will resolve to the current summary contents."></textarea>
</div>
<label for="memory_position">Injection Position</label>
<div class="radio_group">
<label>
<input type="radio" name="memory_position" value="2" />
Before Main Prompt / Story String
</label>
<label>
<input type="radio" name="memory_position" value="0" />
After Main Prompt / Story String
</label>
<label class="flex-container alignItemsCenter" title="How many messages before the current end of the chat." data-i18n="[title]How many messages before the current end of the chat.">
<input type="radio" name="memory_position" value="1" />
In-chat @ Depth <input id="memory_depth" class="text_pole widthUnset" type="number" min="0" max="999" />
as
<select id="memory_role" class="text_pole widthNatural">
<option value="0">System</option>
<option value="1">User</option>
<option value="2">Assistant</option>
</select>
</label>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -24,4 +24,14 @@ label[for="memory_frozen"] input {
flex-direction: row;
align-items: center;
justify-content: space-between;
}
}
.memory_disabled_hint {
margin-left: 2px;
}
#summarySettingsBlock {
display: flex;
flex-direction: column;
row-gap: 5px;
}

View File

@ -47,6 +47,7 @@ const sources = {
openai: 'openai',
comfy: 'comfy',
togetherai: 'togetherai',
drawthings: 'drawthings',
};
const generationMode = {
@ -217,6 +218,9 @@ const defaultSettings = {
vlad_url: 'http://localhost:7860',
vlad_auth: '',
drawthings_url: 'http://localhost:7860',
drawthings_auth: '',
hr_upscaler: 'Latent',
hr_scale: 2.0,
hr_scale_min: 1.0,
@ -314,6 +318,8 @@ function getSdRequestBody() {
return { url: extension_settings.sd.vlad_url, auth: extension_settings.sd.vlad_auth };
case sources.auto:
return { url: extension_settings.sd.auto_url, auth: extension_settings.sd.auto_auth };
case sources.drawthings:
return { url: extension_settings.sd.drawthings_url, auth: extension_settings.sd.drawthings_auth };
default:
throw new Error('Invalid SD source.');
}
@ -390,6 +396,8 @@ async function loadSettings() {
$('#sd_auto_auth').val(extension_settings.sd.auto_auth);
$('#sd_vlad_url').val(extension_settings.sd.vlad_url);
$('#sd_vlad_auth').val(extension_settings.sd.vlad_auth);
$('#sd_drawthings_url').val(extension_settings.sd.drawthings_url);
$('#sd_drawthings_auth').val(extension_settings.sd.drawthings_auth);
$('#sd_interactive_mode').prop('checked', extension_settings.sd.interactive_mode);
$('#sd_openai_style').val(extension_settings.sd.openai_style);
$('#sd_openai_quality').val(extension_settings.sd.openai_quality);
@ -865,6 +873,16 @@ function onVladAuthInput() {
saveSettingsDebounced();
}
function onDrawthingsUrlInput() {
extension_settings.sd.drawthings_url = $('#sd_drawthings_url').val();
saveSettingsDebounced();
}
function onDrawthingsAuthInput() {
extension_settings.sd.drawthings_auth = $('#sd_drawthings_auth').val();
saveSettingsDebounced();
}
function onHrUpscalerChange() {
extension_settings.sd.hr_upscaler = $('#sd_hr_upscaler').find(':selected').val();
saveSettingsDebounced();
@ -931,6 +949,29 @@ async function validateAutoUrl() {
}
}
async function validateDrawthingsUrl() {
try {
if (!extension_settings.sd.drawthings_url) {
throw new Error('URL is not set.');
}
const result = await fetch('/api/sd/drawthings/ping', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(getSdRequestBody()),
});
if (!result.ok) {
throw new Error('SD Drawthings returned an error.');
}
await loadSettingOptions();
toastr.success('SD Drawthings API connected.');
} catch (error) {
toastr.error(`Could not validate SD Drawthings API: ${error.message}`);
}
}
async function validateVladUrl() {
try {
if (!extension_settings.sd.vlad_url) {
@ -1018,6 +1059,27 @@ async function getAutoRemoteModel() {
}
}
async function getDrawthingsRemoteModel() {
try {
const result = await fetch('/api/sd/drawthings/get-model', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(getSdRequestBody()),
});
if (!result.ok) {
throw new Error('SD DrawThings API returned an error.');
}
const data = await result.text();
return data;
} catch (error) {
console.error(error);
return null;
}
}
async function onVaeChange() {
extension_settings.sd.vae = $('#sd_vae').find(':selected').val();
}
@ -1108,6 +1170,9 @@ async function loadSamplers() {
case sources.auto:
samplers = await loadAutoSamplers();
break;
case sources.drawthings:
samplers = await loadDrawthingsSamplers();
break;
case sources.novel:
samplers = await loadNovelSamplers();
break;
@ -1193,6 +1258,22 @@ async function loadAutoSamplers() {
}
}
async function loadDrawthingsSamplers() {
// The app developer doesn't provide an API to get these yet
return [
'UniPC',
'DPM++ 2M Karras',
'Euler a',
'DPM++ SDE Karras',
'PLMS',
'DDIM',
'LCM',
'Euler A Substep',
'DPM++ SDE Substep',
'TCD',
];
}
async function loadVladSamplers() {
if (!extension_settings.sd.vlad_url) {
return [];
@ -1269,6 +1350,9 @@ async function loadModels() {
case sources.auto:
models = await loadAutoModels();
break;
case sources.drawthings:
models = await loadDrawthingsModels();
break;
case sources.novel:
models = await loadNovelModels();
break;
@ -1405,6 +1489,27 @@ async function loadAutoModels() {
}
}
async function loadDrawthingsModels() {
if (!extension_settings.sd.drawthings_url) {
return [];
}
try {
const currentModel = await getDrawthingsRemoteModel();
if (currentModel) {
extension_settings.sd.model = currentModel;
}
const data = [{ value: currentModel, text: currentModel }];
return data;
} catch (error) {
console.log('Error loading DrawThings API models:', error);
return [];
}
}
async function loadOpenAiModels() {
return [
{ value: 'dall-e-3', text: 'DALL-E 3' },
@ -1527,6 +1632,9 @@ async function loadSchedulers() {
case sources.vlad:
schedulers = ['N/A'];
break;
case sources.drawthings:
schedulers = ['N/A'];
break;
case sources.openai:
schedulers = ['N/A'];
break;
@ -1589,6 +1697,9 @@ async function loadVaes() {
case sources.vlad:
vaes = ['N/A'];
break;
case sources.drawthings:
vaes = ['N/A'];
break;
case sources.openai:
vaes = ['N/A'];
break;
@ -1717,7 +1828,10 @@ function getRawLastMessage() {
continue;
}
return message.mes;
return {
mes: message.mes,
original_avatar: message.original_avatar,
};
}
toastr.warning('No usable messages found.', 'Image Generation');
@ -1725,10 +1839,17 @@ function getRawLastMessage() {
};
const context = getContext();
const lastMessage = getLastUsableMessage(),
characterDescription = context.characters[context.characterId].description,
situation = context.characters[context.characterId].scenario;
return `((${processReply(lastMessage)})), (${processReply(situation)}:0.7), (${processReply(characterDescription)}:0.5)`;
const lastMessage = getLastUsableMessage();
const character = context.groupId
? context.characters.find(c => c.avatar === lastMessage.original_avatar)
: context.characters[context.characterId];
if (!character) {
console.debug('Character not found, using raw message.');
return processReply(lastMessage.mes);
}
return `((${processReply(lastMessage.mes)})), (${processReply(character.scenario)}:0.7), (${processReply(character.description)}:0.5)`;
}
async function generatePicture(args, trigger, message, callback) {
@ -1996,6 +2117,9 @@ async function sendGenerationRequest(generationType, prompt, characterName = nul
case sources.vlad:
result = await generateAutoImage(prefixedPrompt, negativePrompt);
break;
case sources.drawthings:
result = await generateDrawthingsImage(prefixedPrompt, negativePrompt);
break;
case sources.auto:
result = await generateAutoImage(prefixedPrompt, negativePrompt);
break;
@ -2178,6 +2302,42 @@ async function generateAutoImage(prompt, negativePrompt) {
}
}
/**
* Generates an image in Drawthings API using the provided prompt and configuration settings.
*
* @param {string} prompt - The main instruction used to guide the image generation.
* @param {string} negativePrompt - The instruction used to restrict the image generation.
* @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete.
*/
async function generateDrawthingsImage(prompt, negativePrompt) {
const result = await fetch('/api/sd/drawthings/generate', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({
...getSdRequestBody(),
prompt: prompt,
negative_prompt: negativePrompt,
sampler_name: extension_settings.sd.sampler,
steps: extension_settings.sd.steps,
cfg_scale: extension_settings.sd.scale,
width: extension_settings.sd.width,
height: extension_settings.sd.height,
restore_faces: !!extension_settings.sd.restore_faces,
enable_hr: !!extension_settings.sd.enable_hr,
denoising_strength: extension_settings.sd.denoising_strength,
// TODO: advanced API parameters: hr, upscaler
}),
});
if (result.ok) {
const data = await result.json();
return { format: 'png', data: data.images[0] };
} else {
const text = await result.text();
throw new Error(text);
}
}
/**
* Generates an image in NovelAI API using the provided prompt and configuration settings.
*
@ -2603,6 +2763,8 @@ function isValidState() {
return true;
case sources.auto:
return !!extension_settings.sd.auto_url;
case sources.drawthings:
return !!extension_settings.sd.drawthings_url;
case sources.vlad:
return !!extension_settings.sd.vlad_url;
case sources.novel:
@ -2745,6 +2907,9 @@ jQuery(async () => {
$('#sd_auto_validate').on('click', validateAutoUrl);
$('#sd_auto_url').on('input', onAutoUrlInput);
$('#sd_auto_auth').on('input', onAutoAuthInput);
$('#sd_drawthings_validate').on('click', validateDrawthingsUrl);
$('#sd_drawthings_url').on('input', onDrawthingsUrlInput);
$('#sd_drawthings_auth').on('input', onDrawthingsAuthInput);
$('#sd_vlad_validate').on('click', validateVladUrl);
$('#sd_vlad_url').on('input', onVladUrlInput);
$('#sd_vlad_auth').on('input', onVladAuthInput);
@ -2756,7 +2921,7 @@ jQuery(async () => {
$('#sd_novel_anlas_guard').on('input', onNovelAnlasGuardInput);
$('#sd_novel_view_anlas').on('click', onViewAnlasClick);
$('#sd_novel_sm').on('input', onNovelSmInput);
$('#sd_novel_sm_dyn').on('input', onNovelSmDynInput);;
$('#sd_novel_sm_dyn').on('input', onNovelSmDynInput);
$('#sd_comfy_validate').on('click', validateComfyUrl);
$('#sd_comfy_url').on('input', onComfyUrlInput);
$('#sd_comfy_workflow').on('change', onComfyWorkflowChange);

View File

@ -36,6 +36,7 @@
<option value="horde">Stable Horde</option>
<option value="auto">Stable Diffusion Web UI (AUTOMATIC1111)</option>
<option value="vlad">SD.Next (vladmandic)</option>
<option value="drawthings">DrawThings HTTP API</option>
<option value="novel">NovelAI Diffusion</option>
<option value="openai">OpenAI (DALL-E)</option>
<option value="comfy">ComfyUI</option>
@ -56,6 +57,21 @@
<input id="sd_auto_auth" type="text" class="text_pole" placeholder="Example: username:password" value="" />
<i><b>Important:</b> run SD Web UI with the <tt>--api</tt> flag! The server must be accessible from the SillyTavern host machine.</i>
</div>
<div data-sd-source="drawthings">
<label for="sd_drawthings_url">DrawThings API URL</label>
<div class="flex-container flexnowrap">
<input id="sd_drawthings_url" type="text" class="text_pole" placeholder="Example: {{drawthings_url}}" value="{{drawthings_url}}" />
<div id="sd_drawthings_validate" class="menu_button menu_button_icon">
<i class="fa-solid fa-check"></i>
<span data-i18n="Connect">
Connect
</span>
</div>
</div>
<label for="sd_drawthings_auth">Authentication (optional)</label>
<input id="sd_drawthings_auth" type="text" class="text_pole" placeholder="Example: username:password" value="" />
<i><b>Important:</b> run DrawThings app with HTTP API switch enabled in the UI! The server must be accessible from the SillyTavern host machine.</i>
</div>
<div data-sd-source="vlad">
<label for="sd_vlad_url">SD.Next API URL</label>
<div class="flex-container flexnowrap">

View File

@ -33,7 +33,7 @@ async function doTokenCounter() {
<div id="tokenized_chunks_display" class="wide100p"></div>
<hr>
<div>Token IDs:</div>
<textarea id="token_counter_ids" class="wide100p textarea_compact" disabled rows="1"></textarea>
<textarea id="token_counter_ids" class="wide100p textarea_compact" readonly rows="1"></textarea>
</div>
</div>`;

View File

@ -2,8 +2,8 @@ import { fuzzySearchCharacters, fuzzySearchGroups, fuzzySearchPersonas, fuzzySea
import { tag_map } from './tags.js';
/**
* The filter types.
* @type {Object.<string, string>}
* The filter types
* @type {{ SEARCH: string, TAG: string, FOLDER: string, FAV: string, GROUP: string, WORLD_INFO_SEARCH: string, PERSONA_SEARCH: string, [key: string]: string }}
*/
export const FILTER_TYPES = {
SEARCH: 'search',
@ -16,25 +16,34 @@ export const FILTER_TYPES = {
};
/**
* The filter states.
* @type {Object.<string, Object>}
* @typedef FilterState One of the filter states
* @property {string} key - The key of the state
* @property {string} class - The css class for this state
*/
/**
* The filter states
* @type {{ SELECTED: FilterState, EXCLUDED: FilterState, UNDEFINED: FilterState, [key: string]: FilterState }}
*/
export const FILTER_STATES = {
SELECTED: { key: 'SELECTED', class: 'selected' },
EXCLUDED: { key: 'EXCLUDED', class: 'excluded' },
UNDEFINED: { key: 'UNDEFINED', class: 'undefined' },
};
/** @type {string} the default filter state of `FILTER_STATES` */
export const DEFAULT_FILTER_STATE = FILTER_STATES.UNDEFINED.key;
/**
* Robust check if one state equals the other. It does not care whether it's the state key or the state value object.
* @param {Object} a First state
* @param {Object} b Second state
* @param {FilterState|string} a First state
* @param {FilterState|string} b Second state
* @returns {boolean}
*/
export function isFilterState(a, b) {
const states = Object.keys(FILTER_STATES);
const aKey = states.includes(a) ? a : states.find(key => FILTER_STATES[key] === a);
const bKey = states.includes(b) ? b : states.find(key => FILTER_STATES[key] === b);
const aKey = typeof a == 'string' && states.includes(a) ? a : states.find(key => FILTER_STATES[key] === a);
const bKey = typeof b == 'string' && states.includes(b) ? b : states.find(key => FILTER_STATES[key] === b);
return aKey === bKey;
}
@ -203,7 +212,7 @@ export class FilterHelper {
return this.filterDataByState(data, state, isFolder);
}
filterDataByState(data, state, filterFunc, { includeFolders } = {}) {
filterDataByState(data, state, filterFunc, { includeFolders = false } = {}) {
if (isFilterState(state, FILTER_STATES.SELECTED)) {
return data.filter(entity => filterFunc(entity) || (includeFolders && entity.type == 'tag'));
}

View File

@ -68,6 +68,7 @@ import {
depth_prompt_depth_default,
loadItemizedPrompts,
animation_duration,
depth_prompt_role_default,
} from '../script.js';
import { printTagList, createTagMapFromList, applyTagsOnCharacterSelect, tag_map } from './tags.js';
import { FILTER_TYPES, FilterHelper } from './filters.js';
@ -284,7 +285,7 @@ export function findGroupMemberId(arg) {
* Gets depth prompts for group members.
* @param {string} groupId Group ID
* @param {number} characterId Current Character ID
* @returns {{depth: number, text: string}[]} Array of depth prompts
* @returns {{depth: number, text: string, role: string}[]} Array of depth prompts
*/
export function getGroupDepthPrompts(groupId, characterId) {
if (!groupId) {
@ -320,9 +321,10 @@ export function getGroupDepthPrompts(groupId, characterId) {
const depthPromptText = baseChatReplace(character.data?.extensions?.depth_prompt?.prompt?.trim(), name1, character.name) || '';
const depthPromptDepth = character.data?.extensions?.depth_prompt?.depth ?? depth_prompt_depth_default;
const depthPromptRole = character.data?.extensions?.depth_prompt?.role ?? depth_prompt_role_default;
if (depthPromptText) {
depthPrompts.push({ text: depthPromptText, depth: depthPromptDepth });
depthPrompts.push({ text: depthPromptText, depth: depthPromptDepth, role: depthPromptRole });
}
}

View File

@ -1,7 +1,8 @@
'use strict';
import { saveSettingsDebounced, substituteParams } from '../script.js';
import { name1, name2, saveSettingsDebounced, substituteParams } from '../script.js';
import { selected_group } from './group-chats.js';
import { parseExampleIntoIndividual } from './openai.js';
import {
power_user,
context_presets,
@ -19,9 +20,13 @@ const controls = [
{ id: 'instruct_system_prompt', property: 'system_prompt', isCheckbox: false },
{ id: 'instruct_system_sequence_prefix', property: 'system_sequence_prefix', isCheckbox: false },
{ id: 'instruct_system_sequence_suffix', property: 'system_sequence_suffix', isCheckbox: false },
{ id: 'instruct_separator_sequence', property: 'separator_sequence', isCheckbox: false },
{ id: 'instruct_input_sequence', property: 'input_sequence', isCheckbox: false },
{ id: 'instruct_input_suffix', property: 'input_suffix', isCheckbox: false },
{ id: 'instruct_output_sequence', property: 'output_sequence', isCheckbox: false },
{ id: 'instruct_output_suffix', property: 'output_suffix', isCheckbox: false },
{ id: 'instruct_system_sequence', property: 'system_sequence', isCheckbox: false },
{ id: 'instruct_system_suffix', property: 'system_suffix', isCheckbox: false },
{ id: 'instruct_user_alignment_message', property: 'user_alignment_message', isCheckbox: false },
{ id: 'instruct_stop_sequence', property: 'stop_sequence', isCheckbox: false },
{ id: 'instruct_names', property: 'names', isCheckbox: true },
{ id: 'instruct_macro', property: 'macro', isCheckbox: true },
@ -31,8 +36,38 @@ const controls = [
{ id: 'instruct_activation_regex', property: 'activation_regex', isCheckbox: false },
{ id: 'instruct_bind_to_context', property: 'bind_to_context', isCheckbox: true },
{ id: 'instruct_skip_examples', property: 'skip_examples', isCheckbox: true },
{ id: 'instruct_system_same_as_user', property: 'system_same_as_user', isCheckbox: true, trigger: true },
];
/**
* Migrates instruct mode settings into the evergreen format.
* @param {object} settings Instruct mode settings.
* @returns {void}
*/
function migrateInstructModeSettings(settings) {
// Separator sequence => Output suffix
if (settings.separator_sequence !== undefined) {
settings.output_suffix = settings.separator_sequence || '';
delete settings.separator_sequence;
}
const defaults = {
input_suffix: '',
system_sequence: '',
system_suffix: '',
user_alignment_message: '',
names_force_groups: true,
skip_examples: false,
system_same_as_user: false,
};
for (let key in defaults) {
if (settings[key] === undefined) {
settings[key] = defaults[key];
}
}
}
/**
* Loads instruct mode settings from the given data object.
* @param {object} data Settings data object.
@ -42,13 +77,7 @@ export function loadInstructMode(data) {
instruct_presets = data.instruct;
}
if (power_user.instruct.names_force_groups === undefined) {
power_user.instruct.names_force_groups = true;
}
if (power_user.instruct.skip_examples === undefined) {
power_user.instruct.skip_examples = false;
}
migrateInstructModeSettings(power_user.instruct);
controls.forEach(control => {
const $element = $(`#${control.id}`);
@ -66,6 +95,10 @@ export function loadInstructMode(data) {
resetScrollHeight($element);
}
});
if (control.trigger) {
$element.trigger('input');
}
});
instruct_presets.forEach((preset) => {
@ -210,12 +243,14 @@ export function getInstructStoppingSequences() {
const result = [];
if (power_user.instruct.enabled) {
const input_sequence = power_user.instruct.input_sequence;
const output_sequence = power_user.instruct.output_sequence;
const first_output_sequence = power_user.instruct.first_output_sequence;
const last_output_sequence = power_user.instruct.last_output_sequence;
const stop_sequence = power_user.instruct.stop_sequence;
const input_sequence = power_user.instruct.input_sequence.replace(/{{name}}/gi, name1);
const output_sequence = power_user.instruct.output_sequence.replace(/{{name}}/gi, name2);
const first_output_sequence = power_user.instruct.first_output_sequence.replace(/{{name}}/gi, name2);
const last_output_sequence = power_user.instruct.last_output_sequence.replace(/{{name}}/gi, name2);
const system_sequence = power_user.instruct.system_sequence.replace(/{{name}}/gi, 'System');
const combined_sequence = `${input_sequence}\n${output_sequence}\n${first_output_sequence}\n${last_output_sequence}`;
const combined_sequence = `${stop_sequence}\n${input_sequence}\n${output_sequence}\n${first_output_sequence}\n${last_output_sequence}\n${system_sequence}`;
combined_sequence.split('\n').filter((line, index, self) => self.indexOf(line) === index).forEach(addInstructSequence);
}
@ -257,26 +292,52 @@ export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvata
includeNames = true;
}
let sequence = (isUser || isNarrator) ? power_user.instruct.input_sequence : power_user.instruct.output_sequence;
if (forceOutputSequence && sequence === power_user.instruct.output_sequence) {
if (forceOutputSequence === force_output_sequence.FIRST && power_user.instruct.first_output_sequence) {
sequence = power_user.instruct.first_output_sequence;
} else if (forceOutputSequence === force_output_sequence.LAST && power_user.instruct.last_output_sequence) {
sequence = power_user.instruct.last_output_sequence;
function getPrefix() {
if (isNarrator) {
return power_user.instruct.system_same_as_user ? power_user.instruct.input_sequence : power_user.instruct.system_sequence;
}
if (isUser) {
return power_user.instruct.input_sequence;
}
if (forceOutputSequence === force_output_sequence.FIRST) {
return power_user.instruct.first_output_sequence || power_user.instruct.output_sequence;
}
if (forceOutputSequence === force_output_sequence.LAST) {
return power_user.instruct.last_output_sequence || power_user.instruct.output_sequence;
}
return power_user.instruct.output_sequence;
}
function getSuffix() {
if (isNarrator) {
return power_user.instruct.system_same_as_user ? power_user.instruct.input_suffix : power_user.instruct.system_suffix;
}
if (isUser) {
return power_user.instruct.input_suffix;
}
return power_user.instruct.output_suffix;
}
let prefix = getPrefix() || '';
let suffix = getSuffix() || '';
if (power_user.instruct.macro) {
sequence = substituteParams(sequence, name1, name2);
sequence = sequence.replace(/{{name}}/gi, name || 'System');
prefix = substituteParams(prefix, name1, name2);
prefix = prefix.replace(/{{name}}/gi, name || 'System');
}
if (!suffix && power_user.instruct.wrap) {
suffix = '\n';
}
const separator = power_user.instruct.wrap ? '\n' : '';
const separatorSequence = power_user.instruct.separator_sequence && !isUser
? power_user.instruct.separator_sequence
: separator;
const textArray = includeNames ? [sequence, `${name}: ${mes}` + separatorSequence] : [sequence, mes + separatorSequence];
const textArray = includeNames ? [prefix, `${name}: ${mes}` + suffix] : [prefix, mes + suffix];
const text = textArray.filter(x => x).join(separator);
return text;
}
@ -286,7 +347,7 @@ export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvata
* @param {string} systemPrompt System prompt string.
* @returns {string} Formatted instruct mode system prompt.
*/
export function formatInstructModeSystemPrompt(systemPrompt){
export function formatInstructModeSystemPrompt(systemPrompt) {
const separator = power_user.instruct.wrap ? '\n' : '';
if (power_user.instruct.system_sequence_prefix) {
@ -302,33 +363,73 @@ export function formatInstructModeSystemPrompt(systemPrompt){
/**
* Formats example messages according to instruct mode settings.
* @param {string} mesExamples Example messages string.
* @param {string[]} mesExamplesArray Example messages array.
* @param {string} name1 User name.
* @param {string} name2 Character name.
* @returns {string} Formatted example messages string.
* @returns {string[]} Formatted example messages string.
*/
export function formatInstructModeExamples(mesExamples, name1, name2) {
export function formatInstructModeExamples(mesExamplesArray, name1, name2) {
const blockHeading = power_user.context.example_separator ? power_user.context.example_separator + '\n' : '';
if (power_user.instruct.skip_examples) {
return mesExamples;
return mesExamplesArray.map(x => x.replace(/<START>\n/i, blockHeading));
}
const includeNames = power_user.instruct.names || (!!selected_group && power_user.instruct.names_force_groups);
let inputSequence = power_user.instruct.input_sequence;
let outputSequence = power_user.instruct.output_sequence;
let inputPrefix = power_user.instruct.input_sequence || '';
let outputPrefix = power_user.instruct.output_sequence || '';
let inputSuffix = power_user.instruct.input_suffix || '';
let outputSuffix = power_user.instruct.output_suffix || '';
if (power_user.instruct.macro) {
inputSequence = substituteParams(inputSequence, name1, name2);
outputSequence = substituteParams(outputSequence, name1, name2);
inputPrefix = substituteParams(inputPrefix, name1, name2);
outputPrefix = substituteParams(outputPrefix, name1, name2);
inputSuffix = substituteParams(inputSuffix, name1, name2);
outputSuffix = substituteParams(outputSuffix, name1, name2);
inputPrefix = inputPrefix.replace(/{{name}}/gi, name1);
outputPrefix = outputPrefix.replace(/{{name}}/gi, name2);
if (!inputSuffix && power_user.instruct.wrap) {
inputSuffix = '\n';
}
if (!outputSuffix && power_user.instruct.wrap) {
outputSuffix = '\n';
}
}
const separator = power_user.instruct.wrap ? '\n' : '';
const separatorSequence = power_user.instruct.separator_sequence ? power_user.instruct.separator_sequence : separator;
const formattedExamples = [];
mesExamples = mesExamples.replace(new RegExp(`\n${name1}: `, 'gm'), separatorSequence + inputSequence + separator + (includeNames ? `${name1}: ` : ''));
mesExamples = mesExamples.replace(new RegExp(`\n${name2}: `, 'gm'), separator + outputSequence + separator + (includeNames ? `${name2}: ` : ''));
for (const item of mesExamplesArray) {
const cleanedItem = item.replace(/<START>/i, '{Example Dialogue:}').replace(/\r/gm, '');
const blockExamples = parseExampleIntoIndividual(cleanedItem);
return mesExamples;
if (blockExamples.length === 0) {
continue;
}
if (blockHeading) {
formattedExamples.push(blockHeading);
}
for (const example of blockExamples) {
const prefix = example.name == 'example_user' ? inputPrefix : outputPrefix;
const suffix = example.name == 'example_user' ? inputSuffix : outputSuffix;
const name = example.name == 'example_user' ? name1 : name2;
const messageContent = includeNames ? `${name}: ${example.content}` : example.content;
const formattedMessage = [prefix, messageContent + suffix].filter(x => x).join(separator);
formattedExamples.push(formattedMessage);
}
}
if (formattedExamples.length === 0) {
return mesExamplesArray.map(x => x.replace(/<START>\n/i, blockHeading));
}
return formattedExamples;
}
/**
@ -338,12 +439,34 @@ export function formatInstructModeExamples(mesExamples, name1, name2) {
* @param {string} promptBias Prompt bias string.
* @param {string} name1 User name.
* @param {string} name2 Character name.
* @param {boolean} isQuiet Is quiet mode generation.
* @param {boolean} isQuietToLoud Is quiet to loud generation.
* @returns {string} Formatted instruct mode last prompt line.
*/
export function formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2) {
const includeNames = name && (power_user.instruct.names || (!!selected_group && power_user.instruct.names_force_groups));
const getOutputSequence = () => power_user.instruct.last_output_sequence || power_user.instruct.output_sequence;
let sequence = isImpersonate ? power_user.instruct.input_sequence : getOutputSequence();
export function formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2, isQuiet, isQuietToLoud) {
const includeNames = name && (power_user.instruct.names || (!!selected_group && power_user.instruct.names_force_groups)) && !(isQuiet && !isQuietToLoud);
function getSequence() {
// User impersonation prompt
if (isImpersonate) {
return power_user.instruct.input_sequence;
}
// Neutral / system prompt
if (isQuiet && !isQuietToLoud) {
return power_user.instruct.output_sequence;
}
// Quiet in-character prompt
if (isQuiet && isQuietToLoud) {
return power_user.instruct.last_output_sequence || power_user.instruct.output_sequence;
}
// Default AI response
return power_user.instruct.last_output_sequence || power_user.instruct.output_sequence;
}
let sequence = getSequence() || '';
if (power_user.instruct.macro) {
sequence = substituteParams(sequence, name1, name2);
@ -353,6 +476,11 @@ export function formatInstructModePrompt(name, isImpersonate, promptBias, name1,
const separator = power_user.instruct.wrap ? '\n' : '';
let text = includeNames ? (separator + sequence + separator + `${name}:`) : (separator + sequence);
// Quiet prompt already has a newline at the end
if (isQuiet && separator) {
text = text.slice(separator.length);
}
if (!isImpersonate && promptBias) {
text += (includeNames ? promptBias : (separator + promptBias.trimStart()));
}
@ -390,15 +518,19 @@ export function replaceInstructMacros(input) {
return '';
}
input = input.replace(/{{instructSystem}}/gi, power_user.instruct.enabled ? power_user.instruct.system_prompt : '');
input = input.replace(/{{instructSystemPrefix}}/gi, power_user.instruct.enabled ? power_user.instruct.system_sequence_prefix : '');
input = input.replace(/{{instructSystemSuffix}}/gi, power_user.instruct.enabled ? power_user.instruct.system_sequence_suffix : '');
input = input.replace(/{{instructInput}}/gi, power_user.instruct.enabled ? power_user.instruct.input_sequence : '');
input = input.replace(/{{instructOutput}}/gi, power_user.instruct.enabled ? power_user.instruct.output_sequence : '');
input = input.replace(/{{instructFirstOutput}}/gi, power_user.instruct.enabled ? (power_user.instruct.first_output_sequence || power_user.instruct.output_sequence) : '');
input = input.replace(/{{instructLastOutput}}/gi, power_user.instruct.enabled ? (power_user.instruct.last_output_sequence || power_user.instruct.output_sequence) : '');
input = input.replace(/{{instructSeparator}}/gi, power_user.instruct.enabled ? power_user.instruct.separator_sequence : '');
input = input.replace(/{{(instructSystem|instructSystemPrompt)}}/gi, power_user.instruct.enabled ? power_user.instruct.system_prompt : '');
input = input.replace(/{{instructSystemPromptPrefix}}/gi, power_user.instruct.enabled ? power_user.instruct.system_sequence_prefix : '');
input = input.replace(/{{instructSystemPromptSuffix}}/gi, power_user.instruct.enabled ? power_user.instruct.system_sequence_suffix : '');
input = input.replace(/{{(instructInput|instructUserPrefix)}}/gi, power_user.instruct.enabled ? power_user.instruct.input_sequence : '');
input = input.replace(/{{instructUserSuffix}}/gi, power_user.instruct.enabled ? power_user.instruct.input_suffix : '');
input = input.replace(/{{(instructOutput|instructAssistantPrefix)}}/gi, power_user.instruct.enabled ? power_user.instruct.output_sequence : '');
input = input.replace(/{{(instructSeparator|instructAssistantSuffix)}}/gi, power_user.instruct.enabled ? power_user.instruct.output_suffix : '');
input = input.replace(/{{instructSystemPrefix}}/gi, power_user.instruct.enabled ? power_user.instruct.system_sequence : '');
input = input.replace(/{{instructSystemSuffix}}/gi, power_user.instruct.enabled ? power_user.instruct.system_suffix : '');
input = input.replace(/{{(instructFirstOutput|instructFirstAssistantPrefix)}}/gi, power_user.instruct.enabled ? (power_user.instruct.first_output_sequence || power_user.instruct.output_sequence) : '');
input = input.replace(/{{(instructLastOutput|instructLastAssistantPrefix)}}/gi, power_user.instruct.enabled ? (power_user.instruct.last_output_sequence || power_user.instruct.output_sequence) : '');
input = input.replace(/{{instructStop}}/gi, power_user.instruct.enabled ? power_user.instruct.stop_sequence : '');
input = input.replace(/{{instructUserFiller}}/gi, power_user.instruct.enabled ? power_user.instruct.user_alignment_message : '');
input = input.replace(/{{exampleSeparator}}/gi, power_user.context.example_separator);
input = input.replace(/{{chatStart}}/gi, power_user.context.chat_start);
@ -420,6 +552,12 @@ jQuery(() => {
saveSettingsDebounced();
});
$('#instruct_system_same_as_user').on('input', function () {
const state = !!$(this).prop('checked');
$('#instruct_system_sequence').prop('disabled', state);
$('#instruct_system_suffix').prop('disabled', state);
});
$('#instruct_enabled').on('change', function () {
if (!power_user.instruct.bind_to_context) {
return;
@ -428,8 +566,8 @@ jQuery(() => {
// When instruct mode gets enabled, select context template matching selected instruct preset
if (power_user.instruct.enabled) {
selectMatchingContextTemplate(power_user.instruct.preset);
// When instruct mode gets disabled, select default context preset
} else {
// When instruct mode gets disabled, select default context preset
selectContextPreset(power_user.default_context);
}
});
@ -442,6 +580,8 @@ jQuery(() => {
return;
}
migrateInstructModeSettings(preset);
power_user.instruct.preset = String(name);
controls.forEach(control => {
if (preset[control.property] !== undefined) {

View File

@ -1,9 +1,12 @@
import { chat, main_api, getMaxContextSize } from '../script.js';
import { timestampToMoment, isDigitsOnly } from './utils.js';
import { chat, main_api, getMaxContextSize, getCurrentChatId } from '../script.js';
import { timestampToMoment, isDigitsOnly, getStringHash } from './utils.js';
import { textgenerationwebui_banned_in_macros } from './textgen-settings.js';
import { replaceInstructMacros } from './instruct-mode.js';
import { replaceVariableMacros } from './variables.js';
// Register any macro that you want to leave in the compiled story string
Handlebars.registerHelper('trim', () => '{{trim}}');
/**
* Returns the ID of the last message in the chat.
* @returns {string} The ID of the last message in the chat.
@ -182,22 +185,14 @@ function getTimeSinceLastMessage() {
}
function randomReplace(input, emptyListPlaceholder = '') {
const randomPatternNew = /{{random\s?::\s?([^}]+)}}/gi;
const randomPatternOld = /{{random\s?:\s?([^}]+)}}/gi;
const randomPattern = /{{random\s?::?([^}]+)}}/gi;
input = input.replace(randomPattern, (match, listString) => {
// Split on either double colons or comma. If comma is the separator, we are also trimming all items.
const list = listString.includes('::')
? listString.split('::')
: listString.split(',').map(item => item.trim());
input = input.replace(randomPatternNew, (match, listString) => {
//split on double colons instead of commas to allow for commas inside random items
const list = listString.split('::').filter(item => item.length > 0);
if (list.length === 0) {
return emptyListPlaceholder;
}
const rng = new Math.seedrandom('added entropy.', { entropy: true });
const randomIndex = Math.floor(rng() * list.length);
//trim() at the end to allow for empty random values
return list[randomIndex].trim();
});
input = input.replace(randomPatternOld, (match, listString) => {
const list = listString.split(',').map(item => item.trim()).filter(item => item.length > 0);
if (list.length === 0) {
return emptyListPlaceholder;
}
@ -208,6 +203,31 @@ function randomReplace(input, emptyListPlaceholder = '') {
return input;
}
function pickReplace(input, rawContent, emptyListPlaceholder = '') {
const pickPattern = /{{pick\s?::?([^}]+)}}/gi;
const chatIdHash = getStringHash(getCurrentChatId());
const rawContentHash = getStringHash(rawContent);
return input.replace(pickPattern, (match, listString, offset) => {
// Split on either double colons or comma. If comma is the separator, we are also trimming all items.
const list = listString.includes('::')
? listString.split('::')
: listString.split(',').map(item => item.trim());
if (list.length === 0) {
return emptyListPlaceholder;
}
// We build a hash seed based on: unique chat file, raw content, and the placement inside this content
// This allows us to get unique but repeatable picks in nearly all cases
const combinedSeedString = `${chatIdHash}-${rawContentHash}-${offset}`;
const finalSeed = getStringHash(combinedSeedString);
const rng = new Math.seedrandom(finalSeed);
const randomIndex = Math.floor(rng() * list.length);
return list[randomIndex];
});
}
function diceRollReplace(input, invalidRollPlaceholder = '') {
const rollPattern = /{{roll[ : ]([^}]+)}}/gi;
@ -242,6 +262,8 @@ export function evaluateMacros(content, env) {
return '';
}
const rawContent = content;
// Legacy non-macro substitutions
content = content.replace(/<USER>/gi, typeof env.user === 'function' ? env.user() : env.user);
content = content.replace(/<BOT>/gi, typeof env.char === 'function' ? env.char() : env.char);
@ -257,6 +279,7 @@ export function evaluateMacros(content, env) {
content = replaceInstructMacros(content);
content = replaceVariableMacros(content);
content = content.replace(/{{newline}}/gi, '\n');
content = content.replace(/\n*{{trim}}\n*/gi, '');
content = content.replace(/{{input}}/gi, () => String($('#send_textarea').val()));
// Substitute passed-in variables
@ -296,5 +319,6 @@ export function evaluateMacros(content, env) {
});
content = bannedWordsReplace(content);
content = randomReplace(content);
content = pickReplace(content, rawContent);
return content;
}

View File

@ -171,6 +171,7 @@ export const chat_completion_sources = {
MAKERSUITE: 'makersuite',
MISTRALAI: 'mistralai',
CUSTOM: 'custom',
COHERE: 'cohere',
};
const character_names_behavior = {
@ -230,6 +231,7 @@ const default_settings = {
google_model: 'gemini-pro',
ai21_model: 'j2-ultra',
mistralai_model: 'mistral-medium-latest',
cohere_model: 'command-r',
custom_model: '',
custom_url: '',
custom_include_body: '',
@ -298,6 +300,7 @@ const oai_settings = {
google_model: 'gemini-pro',
ai21_model: 'j2-ultra',
mistralai_model: 'mistral-medium-latest',
cohere_model: 'command-r',
custom_model: '',
custom_url: '',
custom_include_body: '',
@ -448,8 +451,10 @@ function convertChatCompletionToInstruct(messages, type) {
const isImpersonate = type === 'impersonate';
const isContinue = type === 'continue';
const isQuiet = type === 'quiet';
const isQuietToLoud = false; // Quiet to loud not implemented for Chat Completion
const promptName = isImpersonate ? name1 : name2;
const promptLine = isContinue ? '' : formatInstructModePrompt(promptName, isImpersonate, '', name1, name2).trimStart();
const promptLine = isContinue ? '' : formatInstructModePrompt(promptName, isImpersonate, '', name1, name2, isQuiet, isQuietToLoud).trimStart();
let prompt = [systemPromptText, examplesText, chatMessagesText, promptLine]
.filter(x => x)
@ -523,7 +528,7 @@ function setOpenAIMessageExamples(mesExamplesArray) {
for (let item of mesExamplesArray) {
// remove <START> {Example Dialogue:} and replace \r\n with just \n
let replaced = item.replace(/<START>/i, '{Example Dialogue:}').replace(/\r/gm, '');
let parsed = parseExampleIntoIndividual(replaced);
let parsed = parseExampleIntoIndividual(replaced, true);
// add to the example message blocks array
examples.push(parsed);
}
@ -584,7 +589,13 @@ function setupChatCompletionPromptManager(openAiSettings) {
return promptManager;
}
function parseExampleIntoIndividual(messageExampleString) {
/**
* Parses the example messages into individual messages.
* @param {string} messageExampleString - The string containing the example messages
* @param {boolean} appendNamesForGroup - Whether to append the character name for group chats
* @returns {Message[]} Array of message objects
*/
export function parseExampleIntoIndividual(messageExampleString, appendNamesForGroup = true) {
let result = []; // array of msgs
let tmp = messageExampleString.split('\n');
let cur_msg_lines = [];
@ -597,7 +608,7 @@ function parseExampleIntoIndividual(messageExampleString) {
// strip to remove extra spaces
let parsed_msg = cur_msg_lines.join('\n').replace(name + ':', '').trim();
if (selected_group && ['example_user', 'example_assistant'].includes(system_name)) {
if (appendNamesForGroup && selected_group && ['example_user', 'example_assistant'].includes(system_name)) {
parsed_msg = `${name}: ${parsed_msg}`;
}
@ -807,18 +818,20 @@ function populateDialogueExamples(prompts, chatCompletion, messageExamples) {
if (chatCompletion.canAfford(newExampleChat)) chatCompletion.insert(newExampleChat, 'dialogueExamples');
dialogue.forEach((prompt, promptIndex) => {
for (let promptIndex = 0; promptIndex < dialogue.length; promptIndex++) {
const prompt = dialogue[promptIndex];
const role = 'system';
const content = prompt.content || '';
const identifier = `dialogueExamples ${dialogueIndex}-${promptIndex}`;
const chatMessage = new Message(role, content, identifier);
chatMessage.setName(prompt.name);
if (chatCompletion.canAfford(chatMessage)) {
chatCompletion.insert(chatMessage, 'dialogueExamples');
examplesAdded++;
if (!chatCompletion.canAfford(chatMessage)) {
break;
}
});
chatCompletion.insert(chatMessage, 'dialogueExamples');
examplesAdded++;
}
if (0 === examplesAdded) {
chatCompletion.removeLastFrom('dialogueExamples');
@ -1376,6 +1389,8 @@ function getChatCompletionModel() {
return oai_settings.mistralai_model;
case chat_completion_sources.CUSTOM:
return oai_settings.custom_model;
case chat_completion_sources.COHERE:
return oai_settings.cohere_model;
default:
throw new Error(`Unknown chat completion source: ${oai_settings.chat_completion_source}`);
}
@ -1595,6 +1610,7 @@ async function sendOpenAIRequest(type, messages, signal) {
const isOAI = oai_settings.chat_completion_source == chat_completion_sources.OPENAI;
const isMistral = oai_settings.chat_completion_source == chat_completion_sources.MISTRALAI;
const isCustom = oai_settings.chat_completion_source == chat_completion_sources.CUSTOM;
const isCohere = oai_settings.chat_completion_source == chat_completion_sources.COHERE;
const isTextCompletion = (isOAI && textCompletionModels.includes(oai_settings.openai_model)) || (isOpenRouter && oai_settings.openrouter_force_instruct && power_user.instruct.enabled);
const isQuiet = type === 'quiet';
const isImpersonate = type === 'impersonate';
@ -1729,7 +1745,17 @@ async function sendOpenAIRequest(type, messages, signal) {
generate_data['custom_include_headers'] = oai_settings.custom_include_headers;
}
if ((isOAI || isOpenRouter || isMistral || isCustom) && oai_settings.seed >= 0) {
if (isCohere) {
// Clamp to 0.01 -> 0.99
generate_data['top_p'] = Math.min(Math.max(Number(oai_settings.top_p_openai), 0.01), 0.99);
generate_data['top_k'] = Number(oai_settings.top_k_openai);
// Clamp to 0 -> 1
generate_data['frequency_penalty'] = Math.min(Math.max(Number(oai_settings.freq_pen_openai), 0), 1);
generate_data['presence_penalty'] = Math.min(Math.max(Number(oai_settings.pres_pen_openai), 0), 1);
generate_data['stop'] = getCustomStoppingStrings(5);
}
if ((isOAI || isOpenRouter || isMistral || isCustom || isCohere) && oai_settings.seed >= 0) {
generate_data['seed'] = oai_settings.seed;
}
@ -2225,8 +2251,12 @@ export class ChatCompletion {
continue;
}
if (!excludeList.includes(message.identifier) && message.role === 'system' && !message.name) {
if (lastMessage && lastMessage.role === 'system') {
const shouldSquash = (message) => {
return !excludeList.includes(message.identifier) && message.role === 'system' && !message.name;
}
if (shouldSquash(message)) {
if (lastMessage && shouldSquash(lastMessage)) {
lastMessage.content += '\n' + message.content;
lastMessage.tokens = tokenHandler.count({ role: lastMessage.role, content: lastMessage.content });
}
@ -2589,6 +2619,7 @@ function loadOpenAISettings(data, settings) {
oai_settings.openrouter_force_instruct = settings.openrouter_force_instruct ?? default_settings.openrouter_force_instruct;
oai_settings.ai21_model = settings.ai21_model ?? default_settings.ai21_model;
oai_settings.mistralai_model = settings.mistralai_model ?? default_settings.mistralai_model;
oai_settings.cohere_model = settings.cohere_model ?? default_settings.cohere_model;
oai_settings.custom_model = settings.custom_model ?? default_settings.custom_model;
oai_settings.custom_url = settings.custom_url ?? default_settings.custom_url;
oai_settings.custom_include_body = settings.custom_include_body ?? default_settings.custom_include_body;
@ -2649,6 +2680,8 @@ function loadOpenAISettings(data, settings) {
$(`#model_ai21_select option[value="${oai_settings.ai21_model}"`).attr('selected', true);
$('#model_mistralai_select').val(oai_settings.mistralai_model);
$(`#model_mistralai_select option[value="${oai_settings.mistralai_model}"`).attr('selected', true);
$('#model_cohere_select').val(oai_settings.cohere_model);
$(`#model_cohere_select option[value="${oai_settings.cohere_model}"`).attr('selected', true);
$('#custom_model_id').val(oai_settings.custom_model);
$('#custom_api_url_text').val(oai_settings.custom_url);
$('#openai_max_context').val(oai_settings.openai_max_context);
@ -2885,6 +2918,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
openrouter_sort_models: settings.openrouter_sort_models,
ai21_model: settings.ai21_model,
mistralai_model: settings.mistralai_model,
cohere_model: settings.cohere_model,
custom_model: settings.custom_model,
custom_url: settings.custom_url,
custom_include_body: settings.custom_include_body,
@ -3273,6 +3307,7 @@ function onSettingsPresetChange() {
openrouter_sort_models: ['#openrouter_sort_models', 'openrouter_sort_models', false],
ai21_model: ['#model_ai21_select', 'ai21_model', false],
mistralai_model: ['#model_mistralai_select', 'mistralai_model', false],
cohere_model: ['#model_cohere_select', 'cohere_model', false],
custom_model: ['#custom_model_id', 'custom_model', false],
custom_url: ['#custom_api_url_text', 'custom_url', false],
custom_include_body: ['#custom_include_body', 'custom_include_body', false],
@ -3488,6 +3523,11 @@ async function onModelChange() {
$('#model_mistralai_select').val(oai_settings.mistralai_model);
}
if ($(this).is('#model_cohere_select')) {
console.log('Cohere model changed to', value);
oai_settings.cohere_model = value;
}
if (value && $(this).is('#model_custom_select')) {
console.log('Custom model changed to', value);
oai_settings.custom_model = value;
@ -3611,6 +3651,26 @@ async function onModelChange() {
$('#temp_openai').attr('max', claude_max_temp).val(oai_settings.temp_openai).trigger('input');
}
if (oai_settings.chat_completion_source === chat_completion_sources.COHERE) {
if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', unlocked_max);
}
else if (['command-light', 'command'].includes(oai_settings.cohere_model)) {
$('#openai_max_context').attr('max', max_4k);
}
else if (['command-light-nightly', 'command-nightly'].includes(oai_settings.cohere_model)) {
$('#openai_max_context').attr('max', max_8k);
}
else if (['command-r'].includes(oai_settings.cohere_model)) {
$('#openai_max_context').attr('max', max_128k);
}
else {
$('#openai_max_context').attr('max', max_4k);
}
oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
}
if (oai_settings.chat_completion_source == chat_completion_sources.AI21) {
if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', unlocked_max);
@ -3804,6 +3864,19 @@ async function onConnectButtonClick(e) {
}
}
if (oai_settings.chat_completion_source == chat_completion_sources.COHERE) {
const api_key_cohere = String($('#api_key_cohere').val()).trim();
if (api_key_cohere.length) {
await writeSecret(SECRET_KEYS.COHERE, api_key_cohere);
}
if (!secret_state[SECRET_KEYS.COHERE]) {
console.log('No secret key saved for Cohere');
return;
}
}
startStatusLoading();
saveSettingsDebounced();
await getStatusOpen();
@ -3839,6 +3912,9 @@ function toggleChatCompletionForms() {
else if (oai_settings.chat_completion_source == chat_completion_sources.MISTRALAI) {
$('#model_mistralai_select').trigger('change');
}
else if (oai_settings.chat_completion_source == chat_completion_sources.COHERE) {
$('#model_cohere_select').trigger('change');
}
else if (oai_settings.chat_completion_source == chat_completion_sources.CUSTOM) {
$('#model_custom_select').trigger('change');
}
@ -4491,6 +4567,7 @@ $(document).ready(async function () {
$('#openrouter_sort_models').on('change', onOpenrouterModelSortChange);
$('#model_ai21_select').on('change', onModelChange);
$('#model_mistralai_select').on('change', onModelChange);
$('#model_cohere_select').on('change', onModelChange);
$('#model_custom_select').on('change', onModelChange);
$('#settings_preset_openai').on('change', onSettingsPresetChange);
$('#new_oai_preset').on('click', onNewPresetClick);

View File

@ -10,7 +10,7 @@ import {
eventSource,
event_types,
getCurrentChatId,
printCharacters,
printCharactersDebounced,
setCharacterId,
setEditedMessageId,
renderTemplate,
@ -197,19 +197,26 @@ let power_user = {
preset: 'Alpaca',
system_prompt: 'Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\nWrite {{char}}\'s next reply in a fictional roleplay chat between {{user}} and {{char}}.\n',
input_sequence: '### Instruction:',
input_suffix: '',
output_sequence: '### Response:',
output_suffix: '',
system_sequence: '',
system_suffix: '',
first_output_sequence: '',
last_output_sequence: '',
system_sequence_prefix: '',
system_sequence_suffix: '',
stop_sequence: '',
separator_sequence: '',
wrap: true,
macro: true,
names: false,
names_force_groups: true,
activation_regex: '',
bind_to_context: false,
user_alignment_message: '',
system_same_as_user: false,
/** @deprecated Use output_suffix instead */
separator_sequence: '',
},
default_context: 'Default',
@ -1291,7 +1298,7 @@ async function applyTheme(name) {
key: 'bogus_folders',
action: async () => {
$('#bogus_folders').prop('checked', power_user.bogus_folders);
await printCharacters(true);
printCharactersDebounced();
},
},
{
@ -3049,7 +3056,7 @@ $(document).ready(() => {
$('#show_card_avatar_urls').on('input', function () {
power_user.show_card_avatar_urls = !!$(this).prop('checked');
printCharacters();
printCharactersDebounced();
saveSettingsDebounced();
});
@ -3072,7 +3079,7 @@ $(document).ready(() => {
power_user.sort_field = $(this).find(':selected').data('field');
power_user.sort_order = $(this).find(':selected').data('order');
power_user.sort_rule = $(this).find(':selected').data('rule');
printCharacters();
printCharactersDebounced();
saveSettingsDebounced();
});
@ -3369,15 +3376,15 @@ $(document).ready(() => {
$('#bogus_folders').on('input', function () {
const value = !!$(this).prop('checked');
power_user.bogus_folders = value;
printCharactersDebounced();
saveSettingsDebounced();
printCharacters(true);
});
$('#aux_field').on('change', function () {
const value = $(this).find(':selected').val();
power_user.aux_field = String(value);
printCharactersDebounced();
saveSettingsDebounced();
printCharacters(false);
});
$('#stscript_matching').on('change', function () {

View File

@ -22,6 +22,8 @@ export const SECRET_KEYS = {
OOBA: 'api_key_ooba',
NOMICAI: 'api_key_nomicai',
KOBOLDCPP: 'api_key_koboldcpp',
LLAMACPP: 'api_key_llamacpp',
COHERE: 'api_key_cohere',
};
const INPUT_MAP = {
@ -45,6 +47,8 @@ const INPUT_MAP = {
[SECRET_KEYS.DREAMGEN]: '#api_key_dreamgen',
[SECRET_KEYS.NOMICAI]: '#api_key_nomicai',
[SECRET_KEYS.KOBOLDCPP]: '#api_key_koboldcpp',
[SECRET_KEYS.LLAMACPP]: '#api_key_llamacpp',
[SECRET_KEYS.COHERE]: '#api_key_cohere',
};
async function clearSecret() {

View File

@ -233,8 +233,8 @@ parser.addCommand('peek', peekCallback, [], '<span class="monospace">(message in
parser.addCommand('delswipe', deleteSwipeCallback, ['swipedel'], '<span class="monospace">(optional 1-based id)</span> deletes a swipe from the last chat message. If swipe id not provided - deletes the current swipe.', true, true);
parser.addCommand('echo', echoCallback, [], '<span class="monospace">(title=string severity=info/warning/error/success [text])</span> echoes the text to toast message. Useful for pipes debugging.', true, true);
//parser.addCommand('#', (_, value) => '', [], ' a comment, does nothing, e.g. <tt>/# the next three commands switch variables a and b</tt>', true, true);
parser.addCommand('gen', generateCallback, [], '<span class="monospace">(lock=on/off name="System" [prompt])</span> generates text using the provided prompt and passes it to the next command through the pipe, optionally locking user input while generating and allowing to configure the in-prompt name for instruct mode (default = "System").', true, true);
parser.addCommand('genraw', generateRawCallback, [], '<span class="monospace">(lock=on/off [prompt])</span> generates text using the provided prompt and passes it to the next command through the pipe, optionally locking user input while generating. Does not include chat history or character card. Use instruct=off to skip instruct formatting, e.g. <tt>/genraw instruct=off Why is the sky blue?</tt>. Use stop=... with a JSON-serialized array to add one-time custom stop strings, e.g. <tt>/genraw stop=["\\n"] Say hi</tt>', true, true);
parser.addCommand('gen', generateCallback, [], '<span class="monospace">(lock=on/off name="System" length=123 [prompt])</span> generates text using the provided prompt and passes it to the next command through the pipe, optionally locking user input while generating and allowing to configure the in-prompt name for instruct mode (default = "System"). "as" argument controls the role of the output prompt: system (default) or char. If "length" argument is provided as a number in tokens, allows to temporarily override an API response length.', true, true);
parser.addCommand('genraw', generateRawCallback, [], '<span class="monospace">(lock=on/off instruct=on/off stop=[] as=system/char system="system prompt" length=123 [prompt])</span> generates text using the provided prompt and passes it to the next command through the pipe, optionally locking user input while generating. Does not include chat history or character card. Use instruct=off to skip instruct formatting, e.g. <tt>/genraw instruct=off Why is the sky blue?</tt>. Use stop=... with a JSON-serialized array to add one-time custom stop strings, e.g. <tt>/genraw stop=["\\n"] Say hi</tt>. "as" argument controls the role of the output prompt: system (default) or char. "system" argument adds an (optional) system prompt at the start. If "length" argument is provided as a number in tokens, allows to temporarily override an API response length.', true, true);
parser.addCommand('addswipe', addSwipeCallback, ['swipeadd'], '<span class="monospace">(text)</span> adds a swipe to the last chat message.', true, true);
parser.addCommand('abort', abortCallback, [], ' aborts the slash command batch execution', true, true);
parser.addCommand('fuzzy', fuzzyCallback, [], 'list=["a","b","c"] threshold=0.4 (text to search) performs a fuzzy match of each items of list within the text to search. If any item matches then its name is returned. If no item list matches the text to search then no value is returned. The optional threshold (default is 0.4) allows some control over the matching. A low value (min 0.0) means the match is very strict. At 1.0 (max) the match is very loose and probably matches anything. The returned value passes to the next command through the pipe.', true, true); parser.addCommand('pass', (_, arg) => arg, ['return'], '<span class="monospace">(text)</span> passes the text to the next command through the pipe.', true, true);
@ -662,6 +662,10 @@ async function generateRawCallback(args, value) {
// Prevent generate recursion
$('#send_textarea').val('').trigger('input');
const lock = isTrueBoolean(args?.lock);
const as = args?.as || 'system';
const quietToLoud = as === 'char';
const systemPrompt = resolveVariable(args?.system) || '';
const length = Number(resolveVariable(args?.length) ?? 0) || 0;
try {
if (lock) {
@ -669,7 +673,7 @@ async function generateRawCallback(args, value) {
}
setEphemeralStopStrings(resolveVariable(args?.stop));
const result = await generateRaw(value, '', isFalseBoolean(args?.instruct));
const result = await generateRaw(value, '', isFalseBoolean(args?.instruct), quietToLoud, systemPrompt, length);
return result;
} finally {
if (lock) {
@ -688,6 +692,9 @@ async function generateCallback(args, value) {
// Prevent generate recursion
$('#send_textarea').val('').trigger('input');
const lock = isTrueBoolean(args?.lock);
const as = args?.as || 'system';
const quietToLoud = as === 'char';
const length = Number(resolveVariable(args?.length) ?? 0) || 0;
try {
if (lock) {
@ -696,7 +703,7 @@ async function generateCallback(args, value) {
setEphemeralStopStrings(resolveVariable(args?.stop));
const name = args?.name;
const result = await generateQuietPrompt(value, false, false, '', name);
const result = await generateQuietPrompt(value, quietToLoud, false, '', name, length);
return result;
} finally {
if (lock) {
@ -1656,6 +1663,7 @@ function modelCallback(_, model) {
{ id: 'model_google_select', api: 'openai', type: chat_completion_sources.MAKERSUITE },
{ id: 'model_mistralai_select', api: 'openai', type: chat_completion_sources.MISTRALAI },
{ id: 'model_custom_select', api: 'openai', type: chat_completion_sources.CUSTOM },
{ id: 'model_cohere_select', api: 'openai', type: chat_completion_sources.COHERE },
{ id: 'model_novel_select', api: 'novel', type: null },
{ id: 'horde_model', api: 'koboldhorde', type: null },
];

View File

@ -6,16 +6,16 @@ import {
menu_type,
getCharacters,
entitiesFilter,
printCharacters,
printCharactersDebounced,
buildAvatarList,
eventSource,
event_types,
} from '../script.js';
// eslint-disable-next-line no-unused-vars
import { FILTER_TYPES, FILTER_STATES, isFilterState, FilterHelper } from './filters.js';
import { FILTER_TYPES, FILTER_STATES, DEFAULT_FILTER_STATE, isFilterState, FilterHelper } from './filters.js';
import { groupCandidatesFilter, groups, selected_group } from './group-chats.js';
import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay, debounce } from './utils.js';
import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay } from './utils.js';
import { power_user } from './power-user.js';
export {
@ -38,6 +38,7 @@ export {
importTags,
sortTags,
compareTagsForSort,
removeTagFromMap,
};
const CHARACTER_FILTER_SELECTOR = '#rm_characters_block .rm_tag_filter';
@ -47,29 +48,29 @@ function getFilterHelper(listSelector) {
return $(listSelector).is(GROUP_FILTER_SELECTOR) ? groupCandidatesFilter : entitiesFilter;
}
const redrawCharsAndFiltersDebounced = debounce(() => {
printCharacters(false);
printTagFilters(tag_filter_types.character);
printTagFilters(tag_filter_types.group_member);
}, 100);
export const tag_filter_types = {
character: 0,
group_member: 1,
};
/**
* @type {{ FAV: Tag, GROUP: Tag, FOLDER: Tag, VIEW: Tag, HINT: Tag, UNFILTER: Tag }}
* A collection of global actional tags for the filter panel
* */
const ACTIONABLE_TAGS = {
FAV: { id: 1, name: 'Show only favorites', color: 'rgba(255, 255, 0, 0.5)', action: filterByFav, icon: 'fa-solid fa-star', class: 'filterByFavorites' },
GROUP: { id: 0, name: 'Show only groups', color: 'rgba(100, 100, 100, 0.5)', action: filterByGroups, icon: 'fa-solid fa-users', class: 'filterByGroups' },
FOLDER: { id: 4, name: 'Always show folders', color: 'rgba(120, 120, 120, 0.5)', action: filterByFolder, icon: 'fa-solid fa-folder-plus', class: 'filterByFolder' },
VIEW: { id: 2, name: 'Manage tags', color: 'rgba(150, 100, 100, 0.5)', action: onViewTagsListClick, icon: 'fa-solid fa-gear', class: 'manageTags' },
HINT: { id: 3, name: 'Show Tag List', color: 'rgba(150, 100, 100, 0.5)', action: onTagListHintClick, icon: 'fa-solid fa-tags', class: 'showTagList' },
UNFILTER: { id: 5, name: 'Clear all filters', action: onClearAllFiltersClick, icon: 'fa-solid fa-filter-circle-xmark', class: 'clearAllFilters' },
FAV: { id: '1', sort_order: 1, name: 'Show only favorites', color: 'rgba(255, 255, 0, 0.5)', action: filterByFav, icon: 'fa-solid fa-star', class: 'filterByFavorites' },
GROUP: { id: '0', sort_order: 2, name: 'Show only groups', color: 'rgba(100, 100, 100, 0.5)', action: filterByGroups, icon: 'fa-solid fa-users', class: 'filterByGroups' },
FOLDER: { id: '4', sort_order: 3, name: 'Always show folders', color: 'rgba(120, 120, 120, 0.5)', action: filterByFolder, icon: 'fa-solid fa-folder-plus', class: 'filterByFolder' },
VIEW: { id: '2', sort_order: 4, name: 'Manage tags', color: 'rgba(150, 100, 100, 0.5)', action: onViewTagsListClick, icon: 'fa-solid fa-gear', class: 'manageTags' },
HINT: { id: '3', sort_order: 5, name: 'Show Tag List', color: 'rgba(150, 100, 100, 0.5)', action: onTagListHintClick, icon: 'fa-solid fa-tags', class: 'showTagList' },
UNFILTER: { id: '5', sort_order: 6, name: 'Clear all filters', action: onClearAllFiltersClick, icon: 'fa-solid fa-filter-circle-xmark', class: 'clearAllFilters' },
};
/** @type {{[key: string]: Tag}} An optional list of actionables that can be utilized by extensions */
const InListActionable = {
};
/** @type {Tag[]} A list of default tags */
const DEFAULT_TAGS = [
{ id: uuidv4(), name: 'Plain Text', create_date: Date.now() },
{ id: uuidv4(), name: 'OpenAI', create_date: Date.now() },
@ -79,6 +80,20 @@ const DEFAULT_TAGS = [
{ id: uuidv4(), name: 'AliChat', create_date: Date.now() },
];
/**
* @typedef FolderType Bogus folder type
* @property {string} icon - The icon as a string representation / character
* @property {string} class - The class to apply to the folder type element
* @property {string} [fa_icon] - Optional font-awesome icon class representing the folder type element
* @property {string} [tooltip] - Optional tooltip for the folder type element
* @property {string} [color] - Optional color for the folder type element
* @property {string} [size] - A string representation of the size that the folder type element should be
*/
/**
* @type {{ OPEN: FolderType, CLOSED: FolderType, NONE: FolderType, [key: string]: FolderType }}
* The list of all possible tag folder types
*/
const TAG_FOLDER_TYPES = {
OPEN: { icon: '✔', class: 'folder_open', fa_icon: 'fa-folder-open', tooltip: 'Open Folder (Show all characters even if not selected)', color: 'green', size: '1' },
CLOSED: { icon: '👁', class: 'folder_closed', fa_icon: 'fa-eye-slash', tooltip: 'Closed Folder (Hide all characters unless selected)', color: 'lightgoldenrodyellow', size: '0.7' },
@ -86,8 +101,32 @@ const TAG_FOLDER_TYPES = {
};
const TAG_FOLDER_DEFAULT_TYPE = 'NONE';
/**
* @typedef {object} Tag - Object representing a tag
* @property {string} id - The id of the tag (As a kind of has string. This is used whenever the tag is referenced or linked, as the name might change)
* @property {string} name - The name of the tag
* @property {string} [folder_type] - The bogus folder type of this tag (based on `TAG_FOLDER_TYPES`)
* @property {string} [filter_state] - The saved state of the filter chosen of this tag (based on `FILTER_STATES`)
* @property {number} [sort_order] - A custom integer representing the sort order if tags are sorted
* @property {string} [color] - The background color of the tag
* @property {string} [color2] - The foreground color of the tag
* @property {number} [create_date] - A number representing the date when this tag was created
*
* @property {function} [action] - An optional function that gets executed when this tag is an actionable tag and is clicked on.
* @property {string} [class] - An optional css class added to the control representing this tag when printed. Used for custom tags in the filters.
* @property {string} [icon] - An optional css class of an icon representing this tag when printed. This will replace the tag name with the icon. Used for custom tags in the filters.
*/
/**
* An list of all tags that are available
* @type {Tag[]}
*/
let tags = [];
/**
* A map representing the key of an entity (character avatar, group id, etc) with a corresponding array of tags this entity has assigned. The array might not exist if no tags were assigned yet.
* @type {Object.<string, string[]?>}
*/
let tag_map = {};
/**
@ -140,6 +179,15 @@ function filterByTagState(entities, { globalDisplayFilters = false, subForEntity
return entities;
}
/**
* Filter a a list of entities based on a given tag, returning all entities that represent "sub entities"
*
* @param {Tag} tag - The to filter the entities for
* @param {object[]} entities - The list of possible entities (tag, group, folder) that should get filtered
* @param {object} param2 - optional parameteres
* @param {boolean} [param2.filterHidden] - Whether hidden entities should be filtered out too
* @returns {object[]} The filtered list of entities that apply to the given tag
*/
function filterTagSubEntities(tag, entities, { filterHidden = true } = {}) {
const filterData = structuredClone(entitiesFilter.getFilterData(FILTER_TYPES.TAG));
@ -164,7 +212,9 @@ function filterTagSubEntities(tag, entities, { filterHidden = true } = {}) {
/**
* Indicates whether a given tag is defined as a folder. Meaning it's neither undefined nor 'NONE'.
* @returns {boolean} If it's a tag folder
*
* @param {Tag} tag - The tag to check
* @returns {boolean} Whether it's a tag folder
*/
function isBogusFolder(tag) {
return tag?.folder_type !== undefined && tag.folder_type !== TAG_FOLDER_DEFAULT_TYPE;
@ -172,6 +222,7 @@ function isBogusFolder(tag) {
/**
* Indicates whether a user is currently in a bogus folder.
*
* @returns {boolean} If currently viewing a folder
*/
function isBogusFolderOpen() {
@ -184,6 +235,7 @@ function isBogusFolderOpen() {
/**
* Function to be called when a specific tag/folder is chosen to "drill down".
*
* @param {*} source The jQuery element clicked when choosing the folder
* @param {string} tagId The tag id that is behind the chosen folder
* @param {boolean} remove Whether the given tag should be removed (otherwise it is added/chosen)
@ -201,31 +253,29 @@ function chooseBogusFolder(source, tagId, remove = false) {
// Instead of manually updating the filter conditions, we just "click" on the filter tag
// We search inside which filter block we are located in and use that one
const FILTER_SELECTOR = ($(source).closest('#rm_characters_block') ?? $(source).closest('#rm_group_chats_block')).find('.rm_tag_filter');
if (remove) {
// Click twice to skip over the 'excluded' state
$(FILTER_SELECTOR).find(`.tag[id=${tagId}]`).trigger('click').trigger('click');
} else {
$(FILTER_SELECTOR).find(`.tag[id=${tagId}]`).trigger('click');
}
const tagElement = $(FILTER_SELECTOR).find(`.tag[id=${tagId}]`);
toggleTagThreeState(tagElement, { stateOverride: !remove ? FILTER_STATES.SELECTED : DEFAULT_FILTER_STATE, simulateClick: true });
}
/**
* Builds the tag block for the specified item.
* @param {Object} item The tag item
*
* @param {Tag} tag The tag item
* @param {*} entities The list ob sub items for this tag
* @param {*} hidden A count of how many sub items are hidden
* @returns The html for the tag block
*/
function getTagBlock(item, entities, hidden = 0) {
function getTagBlock(tag, entities, hidden = 0) {
let count = entities.length;
const tagFolder = TAG_FOLDER_TYPES[item.folder_type];
const tagFolder = TAG_FOLDER_TYPES[tag.folder_type];
const template = $('#bogus_folder_template .bogus_folder_select').clone();
template.addClass(tagFolder.class);
template.attr({ 'tagid': item.id, 'id': `BogusFolder${item.id}` });
template.find('.avatar').css({ 'background-color': item.color, 'color': item.color2 }).attr('title', `[Folder] ${item.name}`);
template.find('.ch_name').text(item.name).attr('title', `[Folder] ${item.name}`);
template.attr({ 'tagid': tag.id, 'id': `BogusFolder${tag.id}` });
template.find('.avatar').css({ 'background-color': tag.color, 'color': tag.color2 }).attr('title', `[Folder] ${tag.name}`);
template.find('.ch_name').text(tag.name).attr('title', `[Folder] ${tag.name}`);
template.find('.bogus_folder_hidden_counter').text(hidden > 0 ? `${hidden} hidden` : '');
template.find('.bogus_folder_counter').text(`${count} ${count != 1 ? 'characters' : 'character'}`);
template.find('.bogus_folder_icon').addClass(tagFolder.fa_icon);
@ -242,6 +292,7 @@ function getTagBlock(item, entities, hidden = 0) {
*/
function filterByFav(filterHelper) {
const state = toggleTagThreeState($(this));
ACTIONABLE_TAGS.FAV.filter_state = state;
filterHelper.setFilterData(FILTER_TYPES.FAV, state);
}
@ -251,6 +302,7 @@ function filterByFav(filterHelper) {
*/
function filterByGroups(filterHelper) {
const state = toggleTagThreeState($(this));
ACTIONABLE_TAGS.GROUP.filter_state = state;
filterHelper.setFilterData(FILTER_TYPES.GROUP, state);
}
@ -260,6 +312,7 @@ function filterByGroups(filterHelper) {
*/
function filterByFolder(filterHelper) {
const state = toggleTagThreeState($(this));
ACTIONABLE_TAGS.FOLDER.filter_state = state;
filterHelper.setFilterData(FILTER_TYPES.FOLDER, state);
}
@ -281,6 +334,13 @@ function createTagMapFromList(listElement, key) {
saveSettingsDebounced();
}
/**
* Gets a list of all tags for a given entity key.
* If you have an entity, you can get it's key easily via `getTagKeyForEntity(entity)`.
*
* @param {string} key - The key for which to get tags via the tag map
* @returns {Tag[]} A list of tags
*/
function getTagsList(key) {
if (!Array.isArray(tag_map[key])) {
tag_map[key] = [];
@ -305,6 +365,9 @@ function getInlineListSelector() {
return null;
}
/**
* Gets the current tag key based on the currently selected character or group
*/
function getTagKey() {
if (selected_group && menu_type === 'group_edit') {
return selected_group;
@ -319,7 +382,8 @@ function getTagKey() {
/**
* Gets the tag key for any provided entity/id/key. If a valid tag key is provided, it just returns this.
* Robust method to find a valid tag key for any entity
* Robust method to find a valid tag key for any entity.
*
* @param {object|number|string} entityOrKey An entity with id property (character, group, tag), or directly an id or tag key.
* @returns {string} The tag key that can be found.
*/
@ -337,6 +401,12 @@ export function getTagKeyForEntity(entityOrKey) {
x = character.avatar;
}
// Uninitialized character tag map
if (character && !(x in tag_map)) {
tag_map[x] = [];
return x;
}
// We should hopefully have a key now. Let's check
if (x in tag_map) {
return x;
@ -347,7 +417,7 @@ export function getTagKeyForEntity(entityOrKey) {
}
function addTagToMap(tagId, characterId = null) {
const key = getTagKey() ?? getTagKeyForEntity(characterId);
const key = characterId !== null && characterId !== undefined ? getTagKeyForEntity(characterId) : getTagKey();
if (!key) {
return;
@ -363,7 +433,7 @@ function addTagToMap(tagId, characterId = null) {
}
function removeTagFromMap(tagId, characterId = null) {
const key = getTagKey() ?? getTagKeyForEntity(characterId);
const key = characterId !== null && characterId !== undefined ? getTagKeyForEntity(characterId) : getTagKey();
if (!key) {
return;
@ -392,7 +462,17 @@ function findTag(request, resolve, listSelector) {
resolve(result);
}
function selectTag(event, ui, listSelector) {
/**
* Select a tag and add it to the list. This function is (mostly) used as an event handler for the tag selector control.
*
* @param {*} event - The event that fired on autocomplete select
* @param {*} ui - An Object with label and value properties for the selected option
* @param {*} listSelector - The selector of the list to print/add to
* @param {object} param1 - Optional parameters for this method call
* @param {PrintTagListOptions} [param1.tagListOptions] - Optional parameters for printing the tag list. Can be set to be consistent with the expected behavior of tags in the list that was defined before.
* @returns {boolean} <c>false</c>, to keep the input clear
*/
function selectTag(event, ui, listSelector, { tagListOptions = {} } = {}) {
let tagName = ui.item.value;
let tag = tags.find(t => t.name === tagName);
@ -414,19 +494,29 @@ function selectTag(event, ui, listSelector) {
addTagToMap(tag.id);
}
printCharactersDebounced();
saveSettingsDebounced();
// add tag to the UI and internal map - we reprint so sorting and new markup is done correctly
printTagList(listSelector, { tagOptions: { removable: true } });
printTagList($(getInlineListSelector()));
// We should manually add the selected tag to the print tag function, so we cover places where the tag list did not automatically include it
tagListOptions.addTag = tag;
printTagFilters(tag_filter_types.character);
printTagFilters(tag_filter_types.group_member);
// add tag to the UI and internal map - we reprint so sorting and new markup is done correctly
printTagList(listSelector, tagListOptions);
const inlineSelector = getInlineListSelector();
if (inlineSelector) {
printTagList($(inlineSelector), tagListOptions);
}
// need to return false to keep the input clear
return false;
}
/**
* Get a list of existing tags matching a list of provided new tag names
*
* @param {string[]} new_tags - A list of strings representing tag names
* @returns List of existing tags
*/
function getExistingTags(new_tags) {
let existing_tags = [];
for (let tag of new_tags) {
@ -470,20 +560,28 @@ async function importTags(imported_char) {
console.debug('added tag to map', tag, imported_char.name);
}
}
saveSettingsDebounced();
// Await the character list, which will automatically reprint it and all tag filters
await getCharacters();
printTagFilters(tag_filter_types.character);
printTagFilters(tag_filter_types.group_member);
// need to return false to keep the input clear
return false;
}
/**
* Creates a new tag with default properties and a randomly generated id
*
* @param {string} tagName - name of the tag
* @returns {Tag}
*/
function createNewTag(tagName) {
const tag = {
id: uuidv4(),
name: tagName,
folder_type: TAG_FOLDER_DEFAULT_TYPE,
filter_state: DEFAULT_FILTER_STATE,
sort_order: tags.length,
color: '',
color2: '',
@ -494,7 +592,7 @@ function createNewTag(tagName) {
}
/**
* @typedef {object} TagOptions
* @typedef {object} TagOptions - Options for tag behavior. (Same object will be passed into "appendTagToList")
* @property {boolean} [removable=false] - Whether tags can be removed.
* @property {boolean} [selectable=false] - Whether tags can be selected.
* @property {function} [action=undefined] - Action to perform on tag interaction.
@ -503,28 +601,43 @@ function createNewTag(tagName) {
*/
/**
* Prints the list of tags.
* @param {JQuery<HTMLElement>} element - The container element where the tags are to be printed.
* @param {object} [options] - Optional parameters for printing the tag list.
* @param {Array<object>} [options.tags] Optional override of tags that should be printed. Those will not be sorted. If no supplied, tags for the relevant character are printed.
* @param {object|number|string} [options.forEntityOrKey=undefined] - Optional override for the chosen entity, otherwise the currently selected is chosen. Can be an entity with id property (character, group, tag), or directly an id or tag key.
* @param {boolean} [options.empty=true] - Whether the list should be initially empty.
* @param {function(object): function} [options.tagActionSelector=undefined] - An optional override for the action property that can be assigned to each tag via tagOptions.
* @typedef {object} PrintTagListOptions - Optional parameters for printing the tag list.
* @property {Tag[]|function(): Tag[]} [tags=undefined] - Optional override of tags that should be printed. Those will not be sorted. If no supplied, tags for the relevant character are printed. Can also be a function that returns the tags.
* @property {Tag} [addTag=undefined] - Optionally provide a tag that should be manually added to this print. Either to the overriden tag list or the found tags based on the entity/key. Will respect the tag exists check.
* @property {object|number|string} [forEntityOrKey=undefined] - Optional override for the chosen entity, otherwise the currently selected is chosen. Can be an entity with id property (character, group, tag), or directly an id or tag key.
* @property {boolean|string} [empty=true] - Whether the list should be initially empty. If a string string is provided, 'always' will always empty the list, otherwise it'll evaluate to a boolean.
* @property {function(object): function} [tagActionSelector=undefined] - An optional override for the action property that can be assigned to each tag via tagOptions.
* If set, the selector is executed on each tag as input argument. This allows a list of tags to be provided and each tag can have it's action based on the tag object itself.
* @param {TagOptions} [options.tagOptions={}] - Options for tag behavior. (Same object will be passed into "appendTagToList")
* @property {TagOptions} [tagOptions={}] - Options for tag behavior. (Same object will be passed into "appendTagToList")
*/
function printTagList(element, { tags = undefined, forEntityOrKey = undefined, empty = true, tagActionSelector = undefined, tagOptions = {} } = {}) {
const key = forEntityOrKey !== undefined ? getTagKeyForEntity(forEntityOrKey) : getTagKey();
const printableTags = tags ?? getTagsList(key);
if (empty) {
/**
* Prints the list of tags
*
* @param {JQuery<HTMLElement>} element - The container element where the tags are to be printed.
* @param {PrintTagListOptions} [options] - Optional parameters for printing the tag list.
*/
function printTagList(element, { tags = undefined, addTag = undefined, forEntityOrKey = undefined, empty = true, tagActionSelector = undefined, tagOptions = {} } = {}) {
const key = forEntityOrKey !== undefined ? getTagKeyForEntity(forEntityOrKey) : getTagKey();
let printableTags = tags ? (typeof tags === 'function' ? tags() : tags) : getTagsList(key);
if (empty === 'always' || (empty && (printableTags?.length > 0 || key))) {
$(element).empty();
}
if (addTag && (tagOptions.skipExistsCheck || !printableTags.some(x => x.id === addTag.id))) {
printableTags = [...printableTags, addTag];
}
// one last sort, because we might have modified the tag list or manually retrieved it from a function
printableTags = printableTags.sort(compareTagsForSort);
const customAction = typeof tagActionSelector === 'function' ? tagActionSelector : null;
for (const tag of printableTags) {
// If we have a custom action selector, we override that tag options for each tag
if (tagActionSelector && typeof tagActionSelector === 'function') {
const action = tagActionSelector(tag);
if (customAction) {
const action = customAction(tag);
if (action && typeof action !== 'function') {
console.error('The action parameter must return a function for tag.', tag);
} else {
@ -537,10 +650,11 @@ function printTagList(element, { tags = undefined, forEntityOrKey = undefined, e
}
/**
* Appends a tag to the list element.
* @param {JQuery<HTMLElement>} listElement List element.
* @param {object} tag Tag object to append.
* @param {TagOptions} [options={}] - Options for tag behavior.
* Appends a tag to the list element
*
* @param {JQuery<HTMLElement>} listElement - List element
* @param {Tag} tag - Tag object to append
* @param {TagOptions} [options={}] - Options for tag behavior
* @returns {void}
*/
function appendTagToList(listElement, tag, { removable = false, selectable = false, action = undefined, isGeneralList = false, skipExistsCheck = false } = {}) {
@ -570,8 +684,9 @@ function appendTagToList(listElement, tag, { removable = false, selectable = fal
tagElement.find('.tag_name').text('').attr('title', tag.name).addClass(tag.icon);
}
if (tag.excluded && isGeneralList) {
toggleTagThreeState(tagElement, { stateOverride: FILTER_STATES.EXCLUDED });
// If this is a tag for a general list and its either selectable or actionable, lets mark its current state
if ((selectable || action) && isGeneralList) {
toggleTagThreeState(tagElement, { stateOverride: tag.filter_state ?? DEFAULT_FILTER_STATE });
}
if (selectable) {
@ -596,34 +711,37 @@ function onTagFilterClick(listElement) {
let state = toggleTagThreeState($(this));
// Manual undefined check required for three-state boolean
if (existingTag) {
existingTag.excluded = isFilterState(state, FILTER_STATES.EXCLUDED);
existingTag.filter_state = state;
saveSettingsDebounced();
}
// Update bogus folder if applicable
if (isBogusFolder(existingTag)) {
// Update bogus drilldown
if ($(this).hasClass('selected')) {
appendTagToList($('.rm_tag_controls .rm_tag_bogus_drilldown'), existingTag, { removable: true });
} else {
$(listElement).closest('.rm_tag_controls').find(`.rm_tag_bogus_drilldown .tag[id=${tagId}]`).remove();
}
}
// We don't print anything manually, updating the filter will automatically trigger a redraw of all relevant stuff
runTagFilters(listElement);
updateTagFilterIndicator();
}
/**
* Toggle the filter state of a given tag element
*
* @param {JQuery<HTMLElement>} element - The jquery element representing the tag for which the state should be toggled
* @param {object} param1 - Optional parameters
* @param {import('./filters.js').FilterState|string} [param1.stateOverride] - Optional state override to which the state should be toggled to. If not set, the state will move to the next one in the chain.
* @param {boolean} [param1.simulateClick] - Optionally specify that the state should not just be set on the html element, but actually achieved via triggering the "click" on it, which follows up with the general click handlers and reprinting
* @returns {string} The string representing the new state
*/
function toggleTagThreeState(element, { stateOverride = undefined, simulateClick = false } = {}) {
const states = Object.keys(FILTER_STATES);
const overrideKey = states.includes(stateOverride) ? stateOverride : Object.keys(FILTER_STATES).find(key => FILTER_STATES[key] === stateOverride);
// Make it clear we're getting indexes and handling the 'not found' case in one place
function getStateIndex(key, fallback) {
const index = states.indexOf(key);
return index !== -1 ? index : states.indexOf(fallback);
}
const currentStateIndex = states.indexOf(element.attr('data-toggle-state')) ?? states.length - 1;
const targetStateIndex = overrideKey !== undefined ? states.indexOf(overrideKey) : (currentStateIndex + 1) % states.length;
const overrideKey = typeof stateOverride == 'string' && states.includes(stateOverride) ? stateOverride : Object.keys(FILTER_STATES).find(key => FILTER_STATES[key] === stateOverride);
const currentStateIndex = getStateIndex(element.attr('data-toggle-state'), DEFAULT_FILTER_STATE);
const targetStateIndex = overrideKey !== undefined ? getStateIndex(overrideKey, DEFAULT_FILTER_STATE) : (currentStateIndex + 1) % states.length;
if (simulateClick) {
// Calculate how many clicks are needed to go from the current state to the target state
@ -662,10 +780,8 @@ function runTagFilters(listElement) {
}
function printTagFilters(type = tag_filter_types.character) {
const filterData = structuredClone(entitiesFilter.getFilterData(FILTER_TYPES.TAG));
const FILTER_SELECTOR = type === tag_filter_types.character ? CHARACTER_FILTER_SELECTOR : GROUP_FILTER_SELECTOR;
$(FILTER_SELECTOR).empty();
$(FILTER_SELECTOR).siblings('.rm_tag_bogus_drilldown').empty();
// Print all action tags. (Exclude folder if that setting isn't chosen)
const actionTags = Object.values(ACTIONABLE_TAGS).filter(tag => power_user.bogus_folders || tag.id != ACTIONABLE_TAGS.FOLDER.id);
@ -675,18 +791,21 @@ function printTagFilters(type = tag_filter_types.character) {
printTagList($(FILTER_SELECTOR), { empty: false, tags: inListActionTags, tagActionSelector: tag => tag.action, tagOptions: { isGeneralList: true } });
const characterTagIds = Object.values(tag_map).flat();
const tagsToDisplay = tags
.filter(x => characterTagIds.includes(x.id))
.sort(compareTagsForSort);
const tagsToDisplay = tags.filter(x => characterTagIds.includes(x.id)).sort(compareTagsForSort);
printTagList($(FILTER_SELECTOR), { empty: false, tags: tagsToDisplay, tagOptions: { selectable: true, isGeneralList: true } });
runTagFilters(FILTER_SELECTOR);
// Print bogus folder navigation
const bogusDrilldown = $(FILTER_SELECTOR).siblings('.rm_tag_bogus_drilldown');
bogusDrilldown.empty();
if (power_user.bogus_folders && bogusDrilldown.length > 0) {
const filterData = structuredClone(entitiesFilter.getFilterData(FILTER_TYPES.TAG));
const navigatedTags = filterData.selected.map(x => tags.find(t => t.id == x)).filter(x => isBogusFolder(x));
// Simulate clicks on all "selected" tags when we reprint, otherwise their filter gets lost. "excluded" is persisted.
for (const tagId of filterData.selected) {
toggleTagThreeState($(`${FILTER_SELECTOR} .tag[id="${tagId}"]`), { stateOverride: FILTER_STATES.SELECTED, simulateClick: true });
printTagList(bogusDrilldown, { tags: navigatedTags, tagOptions: { removable: true } });
}
runTagFilters(FILTER_SELECTOR);
if (power_user.show_tag_filters) {
$('.rm_tag_controls .showTagList').addClass('selected');
$('.rm_tag_controls').find('.tag:not(.actionable)').show();
@ -729,9 +848,10 @@ function onTagRemoveClick(event) {
$(`${getInlineListSelector()} .tag[id="${tagId}"]`).remove();
printTagFilters(tag_filter_types.character);
printTagFilters(tag_filter_types.group_member);
printCharactersDebounced();
saveSettingsDebounced();
}
// @ts-ignore
@ -766,12 +886,19 @@ function applyTagsOnGroupSelect() {
// Nothing to do here at the moment. Tags in group interface get automatically redrawn.
}
export function createTagInput(inputSelector, listSelector) {
/**
* Create a tag input by enabling the autocomplete feature of a given input element. Tags will be added to the given list.
*
* @param {string} inputSelector - the selector for the tag input control
* @param {string} listSelector - the selector for the list of the tags modified by the input control
* @param {PrintTagListOptions} [tagListOptions] - Optional parameters for printing the tag list. Can be set to be consistent with the expected behavior of tags in the list that was defined before.
*/
export function createTagInput(inputSelector, listSelector, tagListOptions = {}) {
$(inputSelector)
// @ts-ignore
.autocomplete({
source: (i, o) => findTag(i, o, listSelector),
select: (e, u) => selectTag(e, u, listSelector),
select: (e, u) => selectTag(e, u, listSelector, { tagListOptions: tagListOptions }),
minLength: 0,
})
.focus(onTagInputFocus); // <== show tag list on click
@ -853,10 +980,9 @@ function makeTagListDraggable(tagContainer) {
}
});
saveSettingsDebounced();
// If the order of tags in display has changed, we need to redraw some UI elements. Do it debounced so it doesn't block and you can drag multiple tags.
redrawCharsAndFiltersDebounced();
printCharactersDebounced();
saveSettingsDebounced();
};
// @ts-ignore
@ -867,10 +993,23 @@ function makeTagListDraggable(tagContainer) {
});
}
/**
* Sorts the given tags, returning a shallow copy of it
*
* @param {Tag[]} tags - The tags
* @returns {Tag[]} The sorted tags
*/
function sortTags(tags) {
return tags.slice().sort(compareTagsForSort);
}
/**
* Compares two given tags and returns the compare result
*
* @param {Tag} a - First tag
* @param {Tag} b - Second tag
* @returns {number} The compare result
*/
function compareTagsForSort(a, b) {
if (a.sort_order !== undefined && b.sort_order !== undefined) {
return a.sort_order - b.sort_order;
@ -956,8 +1095,9 @@ async function onTagRestoreFileSelect(e) {
}
$('#tag_view_restore_input').val('');
printCharactersDebounced();
saveSettingsDebounced();
printCharacters(true);
onViewTagsListClick();
}
@ -982,7 +1122,8 @@ function onTagsBackupClick() {
function onTagCreateClick() {
const tag = createNewTag('New Tag');
appendViewTagToList($('#tag_view_list .tag_view_list_tags'), tag, []);
printCharacters(false);
printCharactersDebounced();
saveSettingsDebounced();
}
@ -1051,7 +1192,7 @@ function onTagAsFolderClick() {
updateDrawTagFolder(element, tag);
// If folder display has changed, we have to redraw the character list, otherwise this folders state would not change
printCharacters(true);
printCharactersDebounced();
saveSettingsDebounced();
}
@ -1080,13 +1221,14 @@ function onTagDeleteClick() {
const id = $(this).closest('.tag_view_item').attr('id');
for (const key of Object.keys(tag_map)) {
tag_map[key] = tag_map[key].filter(x => x.id !== id);
tag_map[key] = tag_map[key].filter(x => x !== id);
}
const index = tags.findIndex(x => x.id === id);
tags.splice(index, 1);
$(`.tag[id="${id}"]`).remove();
$(`.tag_view_item[id="${id}"]`).remove();
printCharacters(false);
printCharactersDebounced();
saveSettingsDebounced();
}
@ -1164,8 +1306,8 @@ function copyTags(data) {
}
export function initTags() {
createTagInput('#tagInput', '#tagList');
createTagInput('#groupTagInput', '#groupTagList');
createTagInput('#tagInput', '#tagList', { tagOptions: { removable: true } });
createTagInput('#groupTagInput', '#groupTagList', { tagOptions: { removable: true } });
$(document).on('click', '#rm_button_create', onCharacterCreateClick);
$(document).on('click', '#rm_button_group_chats', onGroupCreateClick);

View File

@ -36,6 +36,7 @@
<li><tt>&lcub;&lcub;roll:(formula)&rcub;&rcub;</tt> rolls a dice. (ex: <tt>>&lcub;&lcub;roll:1d6&rcub;&rcub;</tt> will roll a 6-sided dice and return a number between 1 and 6)</li>
<li><tt>&lcub;&lcub;random:(args)&rcub;&rcub;</tt> returns a random item from the list. (ex: <tt>&lcub;&lcub;random:1,2,3,4&rcub;&rcub;</tt> will return 1 of the 4 numbers at random. Works with text lists too.</li>
<li><tt>&lcub;&lcub;random::(arg1)::(arg2)&rcub;&rcub;</tt> alternative syntax for random that allows to use commas in the list items.</li>
<li><tt>&lcub;&lcub;pick::(args)&rcub;&rcub;</tt> picks a random item from the list. Works the same as &lcub;&lcub;random&rcub;&rcub;, with the same possible syntax options, but the pick will stay consistent for this chat once picked and won't be re-rolled on consecutive messages and prompt processing.</li>
<li><tt>&lcub;&lcub;banned "text here"&rcub;&rcub;</tt> dynamically add text in the quotes to banned words sequences, if Text Generation WebUI backend used. Do nothing for others backends. Can be used anywhere (Character description, WI, AN, etc.) Quotes around the text are important.</li>
</ul>
<div>
@ -48,14 +49,18 @@
<li><tt>&lcub;&lcub;maxPrompt&rcub;&rcub;</tt> max allowed prompt length in tokens = (context size - response length)</li>
<li><tt>&lcub;&lcub;exampleSeparator&rcub;&rcub;</tt> context template example dialogues separator</li>
<li><tt>&lcub;&lcub;chatStart&rcub;&rcub;</tt> context template chat start line</li>
<li><tt>&lcub;&lcub;instructSystem&rcub;&rcub;</tt> instruct system prompt</li>
<li><tt>&lcub;&lcub;instructSystemPrefix&rcub;&rcub;</tt> instruct system prompt prefix sequence</li>
<li><tt>&lcub;&lcub;instructSystemSuffix&rcub;&rcub;</tt> instruct system prompt suffix sequence</li>
<li><tt>&lcub;&lcub;instructInput&rcub;&rcub;</tt> instruct user input sequence</li>
<li><tt>&lcub;&lcub;instructOutput&rcub;&rcub;</tt> instruct assistant output sequence</li>
<li><tt>&lcub;&lcub;instructFirstOutput&rcub;&rcub;</tt> instruct assistant first output sequence</li>
<li><tt>&lcub;&lcub;instructLastOutput&rcub;&rcub;</tt> instruct assistant last output sequence</li>
<li><tt>&lcub;&lcub;instructSeparator&rcub;&rcub;</tt> instruct turn separator sequence</li>
<li><tt>&lcub;&lcub;instructSystemPrompt&rcub;&rcub;</tt> instruct system prompt</li>
<li><tt>&lcub;&lcub;instructSystemPromptPrefix&rcub;&rcub;</tt> instruct system prompt prefix sequence</li>
<li><tt>&lcub;&lcub;instructSystemPromptSuffix&rcub;&rcub;</tt> instruct system prompt suffix sequence</li>
<li><tt>&lcub;&lcub;instructUserPrefix&rcub;&rcub;</tt> instruct user prefix sequence</li>
<li><tt>&lcub;&lcub;instructUserSuffix&rcub;&rcub;</tt> instruct user suffix sequence</li>
<li><tt>&lcub;&lcub;instructAssistantPrefix&rcub;&rcub;</tt> instruct assistant prefix sequence</li>
<li><tt>&lcub;&lcub;instructAssistantSuffix&rcub;&rcub;</tt> instruct assistant suffix sequence</li>
<li><tt>&lcub;&lcub;instructFirstAssistantPrefix&rcub;&rcub;</tt> instruct assistant first output sequence</li>
<li><tt>&lcub;&lcub;instructLastAssistantPrefix&rcub;&rcub;</tt> instruct assistant last output sequence</li>
<li><tt>&lcub;&lcub;instructSystemPrefix&rcub;&rcub;</tt> instruct system message prefix sequence</li>
<li><tt>&lcub;&lcub;instructSystemSuffix&rcub;&rcub;</tt> instruct system message suffix sequence</li>
<li><tt>&lcub;&lcub;instructUserFiller&rcub;&rcub;</tt> instruct first user message filler</li>
<li><tt>&lcub;&lcub;instructStop&rcub;&rcub;</tt> instruct stop sequence</li>
</ul>
<div>

View File

@ -38,7 +38,7 @@ export const textgen_types = {
OPENROUTER: 'openrouter',
};
const { MANCER, APHRODITE, TABBY, TOGETHERAI, OOBA, OLLAMA, LLAMACPP, INFERMATICAI, DREAMGEN, OPENROUTER } = textgen_types;
const { MANCER, APHRODITE, TABBY, TOGETHERAI, OOBA, OLLAMA, LLAMACPP, INFERMATICAI, DREAMGEN, OPENROUTER, KOBOLDCPP } = textgen_types;
const LLAMACPP_DEFAULT_ORDER = [
'top_k',
@ -128,6 +128,7 @@ const settings = {
guidance_scale: 1,
negative_prompt: '',
grammar_string: '',
json_schema: {},
banned_tokens: '',
sampler_priority: OOBA_DEFAULT_ORDER,
samplers: LLAMACPP_DEFAULT_ORDER,
@ -201,6 +202,7 @@ const setting_names = [
'guidance_scale',
'negative_prompt',
'grammar_string',
'json_schema',
'banned_tokens',
'legacy_api',
//'n_aphrodite',
@ -562,6 +564,17 @@ jQuery(function () {
},
});
$('#tabby_json_schema').on('input', function () {
const json_schema_string = String($(this).val());
try {
settings.json_schema = JSON.parse(json_schema_string ?? '{}');
} catch {
// Ignore errors from here
}
saveSettingsDebounced();
});
$('#textgenerationwebui_default_order').on('click', function () {
sortOobaItemsByOrder(OOBA_DEFAULT_ORDER);
settings.sampler_priority = OOBA_DEFAULT_ORDER;
@ -757,6 +770,12 @@ function setSettingByName(setting, value, trigger) {
return;
}
if ('json_schema' === setting) {
settings.json_schema = value ?? {};
$('#tabby_json_schema').val(JSON.stringify(settings.json_schema, null, 2));
return;
}
const isCheckbox = $(`#${setting}_textgenerationwebui`).attr('type') == 'checkbox';
const isText = $(`#${setting}_textgenerationwebui`).attr('type') == 'text' || $(`#${setting}_textgenerationwebui`).is('textarea');
if (isCheckbox) {
@ -984,11 +1003,11 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
'length_penalty': settings.length_penalty,
'early_stopping': settings.early_stopping,
'add_bos_token': settings.add_bos_token,
'dynamic_temperature': settings.dynatemp,
'dynatemp_low': settings.dynatemp ? settings.min_temp : 1,
'dynatemp_high': settings.dynatemp ? settings.max_temp : 1,
'dynatemp_range': settings.dynatemp ? (settings.max_temp - settings.min_temp) / 2 : 0,
'dynatemp_exponent': settings.dynatemp ? settings.dynatemp_exponent : 1,
'dynamic_temperature': settings.dynatemp ? true : undefined,
'dynatemp_low': settings.dynatemp ? settings.min_temp : undefined,
'dynatemp_high': settings.dynatemp ? settings.max_temp : undefined,
'dynatemp_range': settings.dynatemp ? (settings.max_temp - settings.min_temp) / 2 : undefined,
'dynatemp_exponent': settings.dynatemp ? settings.dynatemp_exponent : undefined,
'smoothing_factor': settings.smoothing_factor,
'smoothing_curve': settings.smoothing_curve,
'max_tokens_second': settings.max_tokens_second,
@ -1027,6 +1046,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
'guidance_scale': cfgValues?.guidanceScale?.value ?? settings.guidance_scale ?? 1,
'negative_prompt': cfgValues?.negativePrompt ?? substituteParams(settings.negative_prompt) ?? '',
'grammar_string': settings.grammar_string,
'json_schema': settings.type === TABBY ? settings.json_schema : undefined,
// llama.cpp aliases. In case someone wants to use LM Studio as Text Completion API
'repeat_penalty': settings.rep_pen,
'tfs_z': settings.tfs,
@ -1047,6 +1067,10 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
//'prompt_logprobs': settings.prompt_log_probs_aphrodite,
};
if (settings.type === KOBOLDCPP) {
params.grammar = settings.grammar_string;
}
if (settings.type === MANCER) {
params.n = canMultiSwipe ? settings.n : 1;
params.epsilon_cutoff /= 1000;

View File

@ -37,6 +37,7 @@
--fullred: rgba(255, 0, 0, 1);
--crimson70a: rgba(100, 0, 0, 0.7);
--crimson-hover: rgba(150, 50, 50, 0.5);
--okGreen70a: rgba(0, 100, 0, 0.7);
--cobalt30a: rgba(100, 100, 255, 0.3);
--greyCAIbg: rgb(36, 36, 37);
@ -2109,6 +2110,7 @@ grammarly-extension {
display: flex;
flex-direction: column;
overflow-y: hidden;
overflow-x: hidden;
}
.rm_stat_block {
@ -2129,6 +2131,14 @@ grammarly-extension {
min-width: var(--sheldWidth);
}
.horizontal_scrolling_dialogue_popup {
overflow-x: unset !important;
}
.vertical_scrolling_dialogue_popup {
overflow-y: unset !important;
}
#bulk_tag_popup_holder,
#dialogue_popup_holder {
display: flex;
@ -2151,11 +2161,22 @@ grammarly-extension {
}
#bulk_tag_popup_reset,
#bulk_tag_popup_remove_mutual,
#dialogue_popup_ok {
background-color: var(--crimson70a);
cursor: pointer;
}
#bulk_tag_popup_reset:hover,
#bulk_tag_popup_remove_mutual:hover,
#dialogue_popup_ok:hover {
background-color: var(--crimson-hover);
}
#bulk_tags_avatars_block {
max-height: 70vh;
}
#dialogue_popup_input {
margin: 10px 0;
width: 100%;
@ -3152,7 +3173,7 @@ body.big-avatars .missing-avatar {
}
}
span.warning {
.warning {
color: var(--warning);
font-weight: bolder;
}
@ -3895,6 +3916,7 @@ body:not(.movingUI) .drawer-content.maximized {
.paginationjs-size-changer select {
width: unset;
margin: 0;
font-size: calc(var(--mainFontSize) * 0.85);
}
.paginationjs-pages ul li a {
@ -3924,10 +3946,10 @@ body:not(.movingUI) .drawer-content.maximized {
}
.paginationjs-nav {
padding: 5px;
padding: 2px;
font-size: calc(var(--mainFontSize) * .8);
font-weight: bold;
width: max-content;
width: auto;
}
.onboarding {

0
public/themes/.gitkeep Normal file
View File

View File

@ -30,6 +30,7 @@ const fetch = require('node-fetch').default;
// Unrestrict console logs display limit
util.inspect.defaultOptions.maxArrayLength = null;
util.inspect.defaultOptions.maxStringLength = null;
util.inspect.defaultOptions.depth = 4;
// local library imports
const basicAuthMiddleware = require('./src/middleware/basicAuth');
@ -55,15 +56,29 @@ if (process.versions && process.versions.node && process.versions.node.match(/20
// Set default DNS resolution order to IPv4 first
dns.setDefaultResultOrder('ipv4first');
const DEFAULT_PORT = 8000;
const DEFAULT_AUTORUN = false;
const DEFAULT_LISTEN = false;
const DEFAULT_CORS_PROXY = false;
const cliArguments = yargs(hideBin(process.argv))
.option('autorun', {
.usage('Usage: <your-start-script> <command> [options]')
.option('port', {
type: 'number',
default: null,
describe: `Sets the port under which SillyTavern will run.\nIf not provided falls back to yaml config 'port'.\n[config default: ${DEFAULT_PORT}]`,
}).option('autorun', {
type: 'boolean',
default: false,
describe: 'Automatically launch SillyTavern in the browser.',
default: null,
describe: `Automatically launch SillyTavern in the browser.\nAutorun is automatically disabled if --ssl is set to true.\nIf not provided falls back to yaml config 'autorun'.\n[config default: ${DEFAULT_AUTORUN}]`,
}).option('listen', {
type: 'boolean',
default: null,
describe: `SillyTavern is listening on all network interfaces (Wi-Fi, LAN, localhost). If false, will limit it only to internal localhost (127.0.0.1).\nIf not provided falls back to yaml config 'listen'.\n[config default: ${DEFAULT_LISTEN}]`,
}).option('corsProxy', {
type: 'boolean',
default: false,
describe: 'Enables CORS proxy',
default: null,
describe: `Enables CORS proxy\nIf not provided falls back to yaml config 'enableCorsProxy'.\n[config default: ${DEFAULT_CORS_PROXY}]`,
}).option('disableCsrf', {
type: 'boolean',
default: false,
@ -91,10 +106,11 @@ const app = express();
app.use(compression());
app.use(responseTime());
const server_port = process.env.SILLY_TAVERN_PORT || getConfigValue('port', 8000);
const autorun = (getConfigValue('autorun', false) || cliArguments.autorun) && !cliArguments.ssl;
const listen = getConfigValue('listen', false);
const server_port = cliArguments.port ?? process.env.SILLY_TAVERN_PORT ?? getConfigValue('port', DEFAULT_PORT);
const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN)) && !cliArguments.ssl;
const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN);
const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY);
const basicAuthMode = getConfigValue('basicAuthMode', false);
const { DIRECTORIES, UPLOADS_PATH } = require('./src/constants');
@ -106,9 +122,9 @@ const CORS = cors({
app.use(CORS);
if (listen && getConfigValue('basicAuthMode', false)) app.use(basicAuthMiddleware);
if (listen && basicAuthMode) app.use(basicAuthMiddleware);
app.use(whitelistMiddleware);
app.use(whitelistMiddleware(listen));
// CSRF Protection //
if (!cliArguments.disableCsrf) {
@ -144,7 +160,7 @@ if (!cliArguments.disableCsrf) {
});
}
if (getConfigValue('enableCorsProxy', false) || cliArguments.corsProxy) {
if (enableCorsProxy) {
const bodyParser = require('body-parser');
app.use(bodyParser.json({
limit: '200mb',
@ -500,6 +516,14 @@ const setupTasks = async function () {
if (listen) {
console.log('\n0.0.0.0 means SillyTavern is listening on all network interfaces (Wi-Fi, LAN, localhost). If you want to limit it only to internal localhost (127.0.0.1), change the setting in config.yaml to "listen: false". Check "access.log" file in the SillyTavern directory if you want to inspect incoming connections.\n');
}
if (basicAuthMode) {
const basicAuthUser = getConfigValue('basicAuthUser', {});
if (!basicAuthUser?.username || !basicAuthUser?.password) {
console.warn(color.yellow('Basic Authentication is enabled, but username or password is not set or empty!'));
}
}
};
/**
@ -514,11 +538,11 @@ async function loadPlugins() {
return cleanupPlugins;
} catch {
console.log('Plugin loading failed.');
return () => {};
return () => { };
}
}
if (listen && !getConfigValue('whitelistMode', true) && !getConfigValue('basicAuthMode', false)) {
if (listen && !getConfigValue('whitelistMode', true) && !basicAuthMode) {
if (getConfigValue('securityOverride', false)) {
console.warn(color.red('Security has been overridden. If it\'s not a trusted network, change the settings.'));
}

View File

@ -60,6 +60,14 @@ function getTabbyHeaders() {
}) : {};
}
function getLlamaCppHeaders() {
const apiKey = readSecret(SECRET_KEYS.LLAMACPP);
return apiKey ? ({
'Authorization': `Bearer ${apiKey}`,
}) : {};
}
function getOobaHeaders() {
const apiKey = readSecret(SECRET_KEYS.OOBA);
@ -93,40 +101,21 @@ function getOverrideHeaders(urlHost) {
* @param {string|null} server API server for new request
*/
function setAdditionalHeaders(request, args, server) {
let headers;
const headerGetters = {
[TEXTGEN_TYPES.MANCER]: getMancerHeaders,
[TEXTGEN_TYPES.APHRODITE]: getAphroditeHeaders,
[TEXTGEN_TYPES.TABBY]: getTabbyHeaders,
[TEXTGEN_TYPES.TOGETHERAI]: getTogetherAIHeaders,
[TEXTGEN_TYPES.OOBA]: getOobaHeaders,
[TEXTGEN_TYPES.INFERMATICAI]: getInfermaticAIHeaders,
[TEXTGEN_TYPES.DREAMGEN]: getDreamGenHeaders,
[TEXTGEN_TYPES.OPENROUTER]: getOpenRouterHeaders,
[TEXTGEN_TYPES.KOBOLDCPP]: getKoboldCppHeaders,
[TEXTGEN_TYPES.LLAMACPP]: getLlamaCppHeaders,
};
switch (request.body.api_type) {
case TEXTGEN_TYPES.MANCER:
headers = getMancerHeaders();
break;
case TEXTGEN_TYPES.APHRODITE:
headers = getAphroditeHeaders();
break;
case TEXTGEN_TYPES.TABBY:
headers = getTabbyHeaders();
break;
case TEXTGEN_TYPES.TOGETHERAI:
headers = getTogetherAIHeaders();
break;
case TEXTGEN_TYPES.OOBA:
headers = getOobaHeaders();
break;
case TEXTGEN_TYPES.INFERMATICAI:
headers = getInfermaticAIHeaders();
break;
case TEXTGEN_TYPES.DREAMGEN:
headers = getDreamGenHeaders();
break;
case TEXTGEN_TYPES.OPENROUTER:
headers = getOpenRouterHeaders();
break;
case TEXTGEN_TYPES.KOBOLDCPP:
headers = getKoboldCppHeaders();
break;
default:
headers = {};
break;
}
const getHeaders = headerGetters[request.body.api_type];
const headers = getHeaders ? getHeaders() : {};
if (typeof server === 'string' && server.length > 0) {
try {

View File

@ -162,6 +162,7 @@ const CHAT_COMPLETION_SOURCES = {
MAKERSUITE: 'makersuite',
MISTRALAI: 'mistralai',
CUSTOM: 'custom',
COHERE: 'cohere',
};
const UPLOADS_PATH = './uploads';

View File

@ -1,10 +1,11 @@
const express = require('express');
const fetch = require('node-fetch').default;
const Readable = require('stream').Readable;
const { jsonParser } = require('../../express-common');
const { CHAT_COMPLETION_SOURCES, GEMINI_SAFETY, BISON_SAFETY, OPENROUTER_HEADERS } = require('../../constants');
const { forwardFetchResponse, getConfigValue, tryParse, uuidv4, mergeObjectWithYaml, excludeKeysByYaml, color } = require('../../util');
const { convertClaudeMessages, convertGooglePrompt, convertTextCompletionPrompt } = require('../../prompt-converters');
const { convertClaudeMessages, convertGooglePrompt, convertTextCompletionPrompt, convertCohereMessages } = require('../../prompt-converters');
const { readSecret, SECRET_KEYS } = require('../secrets');
const { getTokenizerModel, getSentencepiceTokenizer, getTiktokenTokenizer, sentencepieceTokenizers, TEXT_COMPLETION_MODELS } = require('../tokenizers');
@ -12,6 +13,61 @@ const { getTokenizerModel, getSentencepiceTokenizer, getTiktokenTokenizer, sente
const API_OPENAI = 'https://api.openai.com/v1';
const API_CLAUDE = 'https://api.anthropic.com/v1';
const API_MISTRAL = 'https://api.mistral.ai/v1';
const API_COHERE = 'https://api.cohere.ai/v1';
/**
* Ollama strikes back. Special boy #2's steaming routine.
* Wrap this abomination into proper SSE stream, again.
* @param {import('node-fetch').Response} jsonStream JSON stream
* @param {import('express').Request} request Express request
* @param {import('express').Response} response Express response
* @returns {Promise<any>} Nothing valuable
*/
async function parseCohereStream(jsonStream, request, response) {
try {
let partialData = '';
jsonStream.body.on('data', (data) => {
const chunk = data.toString();
partialData += chunk;
while (true) {
let json;
try {
json = JSON.parse(partialData);
} catch (e) {
break;
}
if (json.event_type === 'text-generation') {
const text = json.text || '';
const chunk = { choices: [{ text }] };
response.write(`data: ${JSON.stringify(chunk)}\n\n`);
partialData = '';
} else {
partialData = '';
break;
}
}
});
request.socket.on('close', function () {
if (jsonStream.body instanceof Readable) jsonStream.body.destroy();
response.end();
});
jsonStream.body.on('end', () => {
console.log('Streaming request finished');
response.write('data: [DONE]\n\n');
response.end();
});
} catch (error) {
console.log('Error forwarding streaming response:', error);
if (!response.headersSent) {
return response.status(500).send({ error: true });
} else {
return response.end();
}
}
}
/**
* Sends a request to Claude API.
* @param {express.Request} request Express request
@ -460,6 +516,85 @@ async function sendMistralAIRequest(request, response) {
}
}
async function sendCohereRequest(request, response) {
const apiKey = readSecret(SECRET_KEYS.COHERE);
const controller = new AbortController();
request.socket.removeAllListeners('close');
request.socket.on('close', function () {
controller.abort();
});
if (!apiKey) {
console.log('Cohere API key is missing.');
return response.status(400).send({ error: true });
}
try {
const convertedHistory = convertCohereMessages(request.body.messages);
// https://docs.cohere.com/reference/chat
const requestBody = {
stream: Boolean(request.body.stream),
model: request.body.model,
message: convertedHistory.userPrompt,
preamble: convertedHistory.systemPrompt,
chat_history: convertedHistory.chatHistory,
temperature: request.body.temperature,
max_tokens: request.body.max_tokens,
k: request.body.top_k,
p: request.body.top_p,
seed: request.body.seed,
stop_sequences: request.body.stop,
frequency_penalty: request.body.frequency_penalty,
presence_penalty: request.body.presence_penalty,
prompt_truncation: 'AUTO_PRESERVE_ORDER',
connectors: [], // TODO
documents: [],
tools: [],
tool_results: [],
search_queries_only: false,
};
console.log('Cohere request:', requestBody);
const config = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + apiKey,
},
body: JSON.stringify(requestBody),
signal: controller.signal,
timeout: 0,
};
const apiUrl = API_COHERE + '/chat';
if (request.body.stream) {
const stream = await fetch(apiUrl, config);
parseCohereStream(stream, request, response);
} else {
const generateResponse = await fetch(apiUrl, config);
if (!generateResponse.ok) {
console.log(`Cohere API returned error: ${generateResponse.status} ${generateResponse.statusText} ${await generateResponse.text()}`);
// a 401 unauthorized response breaks the frontend auth, so return a 500 instead. prob a better way of dealing with this.
// 401s are already handled by the streaming processor and dont pop up an error toast, that should probably be fixed too.
return response.status(generateResponse.status === 401 ? 500 : generateResponse.status).send({ error: true });
}
const generateResponseJson = await generateResponse.json();
console.log('Cohere response:', generateResponseJson);
return response.send(generateResponseJson);
}
} catch (error) {
console.log('Error communicating with Cohere API: ', error);
if (!response.headersSent) {
response.send({ error: true });
} else {
response.end();
}
}
}
const router = express.Router();
router.post('/status', jsonParser, async function (request, response_getstatus_openai) {
@ -487,6 +622,10 @@ router.post('/status', jsonParser, async function (request, response_getstatus_o
api_key_openai = readSecret(SECRET_KEYS.CUSTOM);
headers = {};
mergeObjectWithYaml(headers, request.body.custom_include_headers);
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.COHERE) {
api_url = API_COHERE;
api_key_openai = readSecret(SECRET_KEYS.COHERE);
headers = {};
} else {
console.log('This chat completion source is not supported yet.');
return response_getstatus_openai.status(400).send({ error: true });
@ -510,6 +649,10 @@ router.post('/status', jsonParser, async function (request, response_getstatus_o
const data = await response.json();
response_getstatus_openai.send(data);
if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.COHERE && Array.isArray(data?.models)) {
data.data = data.models.map(model => ({ id: model.name, ...model }));
}
if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENROUTER && Array.isArray(data?.data)) {
let models = [];
@ -635,6 +778,7 @@ router.post('/generate', jsonParser, function (request, response) {
case CHAT_COMPLETION_SOURCES.AI21: return sendAI21Request(request, response);
case CHAT_COMPLETION_SOURCES.MAKERSUITE: return sendMakerSuiteRequest(request, response);
case CHAT_COMPLETION_SOURCES.MISTRALAI: return sendMistralAIRequest(request, response);
case CHAT_COMPLETION_SOURCES.COHERE: return sendCohereRequest(request, response);
}
let apiUrl;

View File

@ -210,7 +210,8 @@ function convertToV2(char) {
creator: char.creator,
tags: char.tags,
depth_prompt_prompt: char.depth_prompt_prompt,
depth_prompt_response: char.depth_prompt_response,
depth_prompt_depth: char.depth_prompt_depth,
depth_prompt_role: char.depth_prompt_role,
});
result.chat = char.chat ?? humanizedISO8601DateTime();
@ -331,9 +332,12 @@ function charaFormatData(data) {
// Spec extension: depth prompt
const depth_default = 4;
const role_default = 'system';
const depth_value = !isNaN(Number(data.depth_prompt_depth)) ? Number(data.depth_prompt_depth) : depth_default;
const role_value = data.depth_prompt_role ?? role_default;
_.set(char, 'data.extensions.depth_prompt.prompt', data.depth_prompt_prompt ?? '');
_.set(char, 'data.extensions.depth_prompt.depth', depth_value);
_.set(char, 'data.extensions.depth_prompt.role', role_value);
//_.set(char, 'data.extensions.create_date', humanizedISO8601DateTime());
//_.set(char, 'data.extensions.avatar', 'none');
//_.set(char, 'data.extensions.chat', data.ch_name + ' - ' + humanizedISO8601DateTime());

View File

@ -24,7 +24,7 @@ function getDefaultPresets() {
const presets = [];
for (const contentItem of contentIndex) {
if (contentItem.type.endsWith('_preset')) {
if (contentItem.type.endsWith('_preset') || contentItem.type === 'instruct' || contentItem.type === 'context') {
contentItem.name = path.parse(contentItem.filename).name;
contentItem.folder = getTargetByType(contentItem.type);
presets.push(contentItem);
@ -159,6 +159,10 @@ function getTargetByType(type) {
return DIRECTORIES.novelAI_Settings;
case 'textgen_preset':
return DIRECTORIES.textGen_Settings;
case 'instruct':
return DIRECTORIES.instruct;
case 'context':
return DIRECTORIES.context;
default:
return null;
}

View File

@ -34,6 +34,8 @@ const SECRET_KEYS = {
DREAMGEN: 'api_key_dreamgen',
NOMICAI: 'api_key_nomicai',
KOBOLDCPP: 'api_key_koboldcpp',
LLAMACPP: 'api_key_llamacpp',
COHERE: 'api_key_cohere',
};
// These are the keys that are safe to expose, even if allowKeysExposure is false

View File

@ -638,7 +638,80 @@ together.post('/generate', jsonParser, async (request, response) => {
}
});
const drawthings = express.Router();
drawthings.post('/ping', jsonParser, async (request, response) => {
try {
const url = new URL(request.body.url);
url.pathname = '/';
const result = await fetch(url, {
method: 'HEAD',
});
if (!result.ok) {
throw new Error('SD DrawThings API returned an error.');
}
return response.sendStatus(200);
} catch (error) {
console.log(error);
return response.sendStatus(500);
}
});
drawthings.post('/get-model', jsonParser, async (request, response) => {
try {
const url = new URL(request.body.url);
url.pathname = '/';
const result = await fetch(url, {
method: 'GET',
});
const data = await result.json();
return response.send(data['model']);
} catch (error) {
console.log(error);
return response.sendStatus(500);
}
});
drawthings.post('/generate', jsonParser, async (request, response) => {
try {
console.log('SD DrawThings API request:', request.body);
const url = new URL(request.body.url);
url.pathname = '/sdapi/v1/txt2img';
const body = {...request.body};
delete body.url;
const result = await fetch(url, {
method: 'POST',
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json',
'Authorization': getBasicAuthHeader(request.body.auth),
},
timeout: 0,
});
if (!result.ok) {
const text = await result.text();
throw new Error('SD DrawThings API returned an error.', { cause: text });
}
const data = await result.json();
return response.send(data);
} catch (error) {
console.log(error);
return response.sendStatus(500);
}
});
router.use('/comfy', comfy);
router.use('/together', together);
router.use('/drawthings', drawthings);
module.exports = { router };

View File

@ -8,7 +8,6 @@ const { color, getConfigValue } = require('../util');
const whitelistPath = path.join(process.cwd(), './whitelist.txt');
let whitelist = getConfigValue('whitelist', []);
let knownIPs = new Set();
const listen = getConfigValue('listen', false);
const whitelistMode = getConfigValue('whitelistMode', true);
if (fs.existsSync(whitelistPath)) {
@ -34,30 +33,37 @@ function getIpFromRequest(req) {
return clientIp;
}
const whitelistMiddleware = function (req, res, next) {
const clientIp = getIpFromRequest(req);
/**
* Returns a middleware function that checks if the client IP is in the whitelist.
* @param {boolean} listen If listen mode is enabled via config or command line
* @returns {import('express').RequestHandler} The middleware function
*/
function whitelistMiddleware(listen) {
return function (req, res, next) {
const clientIp = getIpFromRequest(req);
if (listen && !knownIPs.has(clientIp)) {
const userAgent = req.headers['user-agent'];
console.log(color.yellow(`New connection from ${clientIp}; User Agent: ${userAgent}\n`));
knownIPs.add(clientIp);
if (listen && !knownIPs.has(clientIp)) {
const userAgent = req.headers['user-agent'];
console.log(color.yellow(`New connection from ${clientIp}; User Agent: ${userAgent}\n`));
knownIPs.add(clientIp);
// Write access log
const timestamp = new Date().toISOString();
const log = `${timestamp} ${clientIp} ${userAgent}\n`;
fs.appendFile('access.log', log, (err) => {
if (err) {
console.error('Failed to write access log:', err);
}
});
}
// Write access log
const timestamp = new Date().toISOString();
const log = `${timestamp} ${clientIp} ${userAgent}\n`;
fs.appendFile('access.log', log, (err) => {
if (err) {
console.error('Failed to write access log:', err);
}
});
}
//clientIp = req.connection.remoteAddress.split(':').pop();
if (whitelistMode === true && !whitelist.some(x => ipMatching.matches(clientIp, ipMatching.getMatch(x)))) {
console.log(color.red('Forbidden: Connection attempt from ' + clientIp + '. If you are attempting to connect, please add your IP address in whitelist or disable whitelist mode in config.yaml in root of SillyTavern folder.\n'));
return res.status(403).send('<b>Forbidden</b>: Connection attempt from <b>' + clientIp + '</b>. If you are attempting to connect, please add your IP address in whitelist or disable whitelist mode in config.yaml in root of SillyTavern folder.');
}
next();
};
//clientIp = req.connection.remoteAddress.split(':').pop();
if (whitelistMode === true && !whitelist.some(x => ipMatching.matches(clientIp, ipMatching.getMatch(x)))) {
console.log(color.red('Forbidden: Connection attempt from ' + clientIp + '. If you are attempting to connect, please add your IP address in whitelist or disable whitelist mode in config.yaml in root of SillyTavern folder.\n'));
return res.status(403).send('<b>Forbidden</b>: Connection attempt from <b>' + clientIp + '</b>. If you are attempting to connect, please add your IP address in whitelist or disable whitelist mode in config.yaml in root of SillyTavern folder.');
}
next();
};
}
module.exports = whitelistMiddleware;

8
src/polyfill.js Normal file
View File

@ -0,0 +1,8 @@
if (!Array.prototype.findLastIndex) {
Array.prototype.findLastIndex = function (callback, thisArg) {
for (let i = this.length - 1; i >= 0; i--) {
if (callback.call(thisArg, this[i], i, this)) return i;
}
return -1;
};
}

View File

@ -1,3 +1,5 @@
require('./polyfill.js');
/**
* Convert a prompt from the ChatML objects to the format used by Claude.
* @param {object[]} messages Array of messages
@ -89,11 +91,16 @@ function convertClaudeMessages(messages, prefillString, useSysPrompt, humanMsgFi
if (messages[i].role !== 'system') {
break;
}
// Append example names if not already done by the frontend (e.g. for group chats).
if (userName && messages[i].name === 'example_user') {
messages[i].content = `${userName}: ${messages[i].content}`;
if (!messages[i].content.startsWith(`${userName}: `)) {
messages[i].content = `${userName}: ${messages[i].content}`;
}
}
if (charName && messages[i].name === 'example_assistant') {
messages[i].content = `${charName}: ${messages[i].content}`;
if (!messages[i].content.startsWith(`${charName}: `)) {
messages[i].content = `${charName}: ${messages[i].content}`;
}
}
systemPrompt += `${messages[i].content}\n\n`;
}
@ -183,6 +190,64 @@ function convertClaudeMessages(messages, prefillString, useSysPrompt, humanMsgFi
return { messages: mergedMessages, systemPrompt: systemPrompt.trim() };
}
/**
* Convert a prompt from the ChatML objects to the format used by Cohere.
* @param {object[]} messages Array of messages
* @param {string} charName Character name
* @param {string} userName User name
* @returns {{systemPrompt: string, chatHistory: object[], userPrompt: string}} Prompt for Cohere
*/
function convertCohereMessages(messages, charName = '', userName = '') {
const roleMap = {
'system': 'SYSTEM',
'user': 'USER',
'assistant': 'CHATBOT',
};
const placeholder = '[Start a new chat]';
let systemPrompt = '';
// Collect all the system messages up until the first instance of a non-system message, and then remove them from the messages array.
let i;
for (i = 0; i < messages.length; i++) {
if (messages[i].role !== 'system') {
break;
}
// Append example names if not already done by the frontend (e.g. for group chats).
if (userName && messages[i].name === 'example_user') {
if (!messages[i].content.startsWith(`${userName}: `)) {
messages[i].content = `${userName}: ${messages[i].content}`;
}
}
if (charName && messages[i].name === 'example_assistant') {
if (!messages[i].content.startsWith(`${charName}: `)) {
messages[i].content = `${charName}: ${messages[i].content}`;
}
}
systemPrompt += `${messages[i].content}\n\n`;
}
messages.splice(0, i);
if (messages.length === 0) {
messages.unshift({
role: 'user',
content: placeholder,
});
}
const lastNonSystemMessageIndex = messages.findLastIndex(msg => msg.role === 'user' || msg.role === 'assistant');
const userPrompt = messages.slice(lastNonSystemMessageIndex).map(msg => msg.content).join('\n\n') || placeholder;
const chatHistory = messages.slice(0, lastNonSystemMessageIndex).map(msg => {
return {
role: roleMap[msg.role] || 'USER',
message: msg.content,
};
});
return { systemPrompt: systemPrompt.trim(), chatHistory, userPrompt };
}
/**
* Convert a prompt from the ChatML objects to the format used by Google MakerSuite models.
* @param {object[]} messages Array of messages
@ -295,4 +360,5 @@ module.exports = {
convertClaudeMessages,
convertGooglePrompt,
convertTextCompletionPrompt,
convertCohereMessages,
};