diff --git a/aiserver.py b/aiserver.py
index de013688..1bfbda85 100644
--- a/aiserver.py
+++ b/aiserver.py
@@ -2163,6 +2163,9 @@ def patch_transformers():
scores: torch.FloatTensor,
**kwargs,
) -> bool:
+ if not koboldai_vars.inference_config.do_core:
+ return False
+
koboldai_vars.generated_tkns += 1
if (
@@ -5006,7 +5009,7 @@ def calcsubmit(txt):
# Send it!
ikrequest(subtxt)
-def core_generate(text: list, min: int, max: int, found_entries: set):
+def core_generate(text: list, min: int, max: int, found_entries: set, is_core: bool = False):
# This generation function is tangled with koboldai_vars intentionally. It
# is meant for the story and nothing else.
@@ -5065,6 +5068,7 @@ def core_generate(text: list, min: int, max: int, found_entries: set):
batch_count=numseqs,
# Real max length is handled by CoreStopper.
bypass_hf_maxlength=True,
+ is_core=True,
)
genout = result.encoded
@@ -5192,9 +5196,11 @@ def raw_generate(
do_dynamic_wi: bool = False,
batch_count: int = 1,
bypass_hf_maxlength: bool = False,
- generation_settings: Optional[dict] = None
+ generation_settings: Optional[dict] = None,
+ is_core: bool = False
) -> GenerationResult:
+ koboldai_vars.inference_config.do_core = is_core
gen_settings = GenerationSettings(*(generation_settings or {}))
model_functions = {
@@ -6140,6 +6146,18 @@ def applyoutputformatting(txt):
if(koboldai_vars.singleline or koboldai_vars.chatmode):
txt = utils.singlelineprocessing(txt, koboldai_vars)
+ for sub in koboldai_vars.substitutions:
+ if not sub["enabled"]:
+ continue
+ i = 0
+ while sub["trueTarget"] in txt or sub["target"] in txt:
+ i += 1
+ if i > 1000:
+ logger.error("[substitutions] Infinite recursion :^(")
+ break
+ txt = txt.replace(sub["trueTarget"], sub["substitution"])
+ txt = txt.replace(sub["target"], sub["substitution"])
+
return txt
#==================================================================#
@@ -8501,6 +8519,11 @@ def UI_2_phrase_bias_update(biases):
koboldai_vars.biases = biases
+@socketio.on("substitution_update")
+@logger.catch
+def UI_2_substitutions_update(substitutions):
+ koboldai_vars.substitutions = substitutions
+
#==================================================================#
# Event triggered to rely a message
diff --git a/koboldai_settings.py b/koboldai_settings.py
index 273c87f8..31e8241a 100644
--- a/koboldai_settings.py
+++ b/koboldai_settings.py
@@ -1,5 +1,6 @@
from dataclasses import dataclass
import os, re, time, threading, json, pickle, base64, copy, tqdm, datetime, sys
+from typing import Union
from io import BytesIO
from flask import has_request_context, session
from flask_socketio import SocketIO, join_room, leave_room
@@ -180,6 +181,18 @@ class koboldai_vars(object):
def reset_model(self):
self._model_settings.reset_for_model_load()
+ def get_token_representation(self, text: Union[str, list, None]) -> list:
+ if not self.tokenizer or not text:
+ return []
+
+ if isinstance(text, str):
+ encoded = self.tokenizer.encode(text)
+ else:
+ encoded = text
+
+ # TODO: This might be ineffecient, should we cache some of this?
+ return [[token, self.tokenizer.decode(token)] for token in encoded]
+
def calc_ai_text(self, submitted_text="", return_text=False):
#start_time = time.time()
if self.alt_gen:
@@ -198,7 +211,7 @@ class koboldai_vars(object):
# TODO: We may want to replace the "text" variable with a list-type
# class of context blocks, the class having a __str__ function.
if self.sp_length > 0:
- context.append({"type": "soft_prompt", "text": f"<{self.sp_length} tokens of Soft Prompt.>", "tokens": self.sp_length})
+ context.append({"type": "soft_prompt", "text": f"<{self.sp_length} tokens of Soft Prompt.>", "tokens": [-1] * self.sp_length})
# Header is never used?
# if koboldai_vars.model not in ("Colab", "API", "OAI") and self.tokenizer._koboldai_header:
# context.append({"type": "header", "text": f"{len(self.tokenizer._koboldai_header})
@@ -208,11 +221,16 @@ class koboldai_vars(object):
#Add memory
memory_length = self.max_memory_length if self.memory_length > self.max_memory_length else self.memory_length
memory_text = self.memory
+ memory_encoded = None
if memory_length+used_tokens <= token_budget:
- if self.tokenizer is not None and self.memory_length > self.max_memory_length:
- memory_text = self.tokenizer.decode(self.tokenizer.encode(self.memory)[-self.max_memory_length-1:])
+ if self.tokenizer is not None and self.memory_length > self.max_memory_length:
+ memory_encoded = self.tokenizer.encode(self.memory)[-self.max_memory_length-1:]
+ memory_text = self.tokenizer.decode(memory_encoded)
+
+ if not memory_encoded and self.tokenizer:
+ memory_encoded = self.tokenizer.encode(memory_text)
- context.append({"type": "memory", "text": memory_text, "tokens": memory_length})
+ context.append({"type": "memory", "text": memory_text, "tokens": self.get_token_representation(memory_encoded)})
text += memory_text
#Add constant world info entries to memory
@@ -223,7 +241,11 @@ class koboldai_vars(object):
used_world_info.append(wi['uid'])
self.worldinfo_v2.set_world_info_used(wi['uid'])
wi_text = wi['content']
- context.append({"type": "world_info", "text": wi_text, "tokens": wi['token_length']})
+ context.append({
+ "type": "world_info",
+ "text": wi_text,
+ "tokens": self.get_token_representation(wi_text),
+ })
text += wi_text
@@ -268,7 +290,7 @@ class koboldai_vars(object):
used_tokens+=0 if wi['token_length'] is None else wi['token_length']
used_world_info.append(wi['uid'])
wi_text = wi['content']
- context.append({"type": "world_info", "text": wi_text, "tokens": wi['token_length']})
+ context.append({"type": "world_info", "text": wi_text, "tokens": self.get_token_representation(wi_text)})
text += wi_text
self.worldinfo_v2.set_world_info_used(wi['uid'])
@@ -288,31 +310,50 @@ class koboldai_vars(object):
game_context = []
authors_note_final = self.authornotetemplate.replace("<|>", self.authornote)
used_all_tokens = False
+
for action in range(len(self.actions)):
self.actions.set_action_in_ai(action, used=False)
+
for i in range(len(action_text_split)-1, -1, -1):
if action_text_split[i][3] or action_text_split[i][1] == [-1]:
#We've hit an item we've already included or items that are only prompt. Stop
for action in action_text_split[i][1]:
if action >= 0:
self.actions.set_action_in_ai(action)
- break;
+ break
+
if len(action_text_split) - i - 1 == self.andepth and self.authornote != "":
game_text = "{}{}".format(authors_note_final, game_text)
- game_context.insert(0, {"type": "authors_note", "text": authors_note_final, "tokens": self.authornote_length})
- length = 0 if self.tokenizer is None else len(self.tokenizer.encode(action_text_split[i][0]))
+ game_context.insert(0, {"type": "authors_note", "text": authors_note_final, "tokens": self.get_token_representation(authors_note_final)})
+
+ encoded_action = [] if not self.tokenizer else self.tokenizer.encode(action_text_split[i][0])
+ length = len(encoded_action)
+
if length+used_tokens <= token_budget and not used_all_tokens:
used_tokens += length
selected_text = action_text_split[i][0]
action_text_split[i][3] = True
game_text = "{}{}".format(selected_text, game_text)
+
if action_text_split[i][1] == [self.actions.action_count+1]:
- game_context.insert(0, {"type": "submit", "text": selected_text, "tokens": length, "action_ids": action_text_split[i][1]})
+ game_context.insert(0, {
+ "type": "submit",
+ "text": selected_text,
+ "tokens": self.get_token_representation(encoded_action),
+ "action_ids": action_text_split[i][1]
+ })
else:
- game_context.insert(0, {"type": "action", "text": selected_text, "tokens": length, "action_ids": action_text_split[i][1]})
+ game_context.insert(0, {
+ "type": "action",
+ "text": selected_text,
+ "tokens": self.get_token_representation(encoded_action),
+ "action_ids": action_text_split[i][1]
+ })
+
for action in action_text_split[i][1]:
if action >= 0:
self.actions.set_action_in_ai(action)
+
#Now we need to check for used world info entries
for wi in self.worldinfo_v2:
if wi['uid'] not in used_world_info:
@@ -336,12 +377,13 @@ class koboldai_vars(object):
used_tokens+=0 if wi['token_length'] is None else wi['token_length']
used_world_info.append(wi['uid'])
wi_text = wi["content"]
+ encoded_wi = self.tokenizer.encode(wi_text)
if method == 1:
text = "{}{}".format(wi_text, game_text)
- context.insert(0, {"type": "world_info", "text": wi_text, "tokens": wi['token_length']})
+ context.insert(0, {"type": "world_info", "text": wi_text, "tokens": self.get_token_representation(encoded_wi)})
else:
game_text = "{}{}".format(wi_text, game_text)
- game_context.insert(0, {"type": "world_info", "text": wi_text, "tokens": wi['token_length']})
+ game_context.insert(0, {"type": "world_info", "text": wi_text, "tokens": self.get_token_representation(encoded_wi)})
self.worldinfo_v2.set_world_info_used(wi['uid'])
else:
used_all_tokens = True
@@ -350,11 +392,11 @@ class koboldai_vars(object):
#if we don't have enough actions to get to author's note depth then we just add it right before the game text
if len(action_text_split) < self.andepth and self.authornote != "":
game_text = "{}{}".format(authors_note_final, game_text)
- game_context.insert(0, {"type": "authors_note", "text": authors_note_final, "tokens": authornote_length})
+ game_context.insert(0, {"type": "authors_note", "text": authors_note_final, "tokens": self.get_token_representation(authors_note_final)})
if self.useprompt:
text += prompt_text
- context.append({"type": "prompt", "text": prompt_text, "tokens": prompt_length})
+ context.append({"type": "prompt", "text": prompt_text, "tokens": self.get_token_representation(prompt_text)})
elif not used_all_tokens:
prompt_length = 0
prompt_text = ""
@@ -392,12 +434,12 @@ class koboldai_vars(object):
used_tokens+=0 if wi['token_length'] is None else wi['token_length']
used_world_info.append(wi['uid'])
wi_text = wi['content']
- context.append({"type": "world_info", "text": wi_text, "tokens": wi['token_length']})
+ context.append({"type": "world_info", "text": wi_text, "tokens": self.get_token_representation(wi_text)})
text += wi_text
self.worldinfo_v2.set_world_info_used(wi['uid'])
text += prompt_text
- context.append({"type": "prompt", "text": prompt_text, "tokens": prompt_length})
+ context.append({"type": "prompt", "text": prompt_text, "tokens": self.get_token_representation(prompt_text)})
self.prompt_in_ai = True
else:
self.prompt_in_ai = False
@@ -723,6 +765,14 @@ class story_settings(settings):
self.revisions = []
self.picture = "" #base64 of the image shown for the story
self.picture_prompt = "" #Prompt used to create picture
+ self.substitutions = [
+ {"target": "--", "substitution": "–", "enabled": False},
+ {"target": "---", "substitution": "—", "enabled": False},
+ {"target": "...", "substitution": "…", "enabled": False},
+ # {"target": "(c)", "substitution": "©", "enabled": False},
+ # {"target": "(r)", "substitution": "®", "enabled": False},
+ # {"target": "(tm)", "substitution": "™", "enabled": False},
+ ]
#must be at bottom
self.no_save = False #Temporary disable save (doesn't save with the file)
@@ -1001,6 +1051,7 @@ class system_settings(settings):
do_dynamic_wi: bool = False
# Genamt stopping is mostly tied to Dynamic WI
stop_at_genamt: bool = False
+ do_core: bool = True
self.inference_config = _inference_config()
self._koboldai_var = koboldai_var
diff --git a/static/koboldai.css b/static/koboldai.css
index 4e3a3593..d16d34ad 100644
--- a/static/koboldai.css
+++ b/static/koboldai.css
@@ -1860,6 +1860,10 @@ body {
height: 100%;
flex-grow: 1;
padding: 0px 10px;
+
+ /* HACK: This is a visually ugly hack to avoid cutting of token tooltips on
+ the first line. */
+ padding-top: 15px;
}
.context-symbol {
@@ -1874,10 +1878,30 @@ body {
font-family: monospace;
}
-.context-block:hover {
+.context-token {
+ position: relative;
+ background-color: inherit;
+}
+
+.context-token:hover {
outline: 1px solid gray;
}
+.context-token:hover::after {
+ content: attr(token-id);
+ position: absolute;
+
+ top: -120%;
+ left: 50%;
+ transform: translateX(-50%);
+
+ padding: 0px 2px;
+ background-color: rgba(0, 0, 0, 0.6);
+
+ pointer-events: none;
+ z-index: 9999999;
+}
+
.context-sp {background-color: var(--context_colors_soft_prompt);}
.context-prompt {background-color: var(--context_colors_prompt);}
.context-wi {background-color: var(--context_colors_world_info);}
@@ -2259,6 +2283,80 @@ body {
margin-right: 5px;
}
+/* Substitutions */
+#Substitutions > .helpicon {
+ margin-left: 5px;
+ align-self: auto;
+}
+
+#substitution-header {
+ display: flex;
+ width: 100%;
+ justify-content: space-around;
+}
+
+#substitution-container {
+ width: 100%;
+}
+
+.substitution-card, #new-sub-card {
+ display: flex;
+
+ column-gap: 5px;
+ height: 30px;
+ width: 100%;
+ padding: 1px 0px;
+}
+
+.substitution-card > .card-section {
+ display: flex;
+}
+
+.substitution-card > .card-left > .material-icons-outlined {
+ margin-left: -5px;
+ margin-right: 5px;
+ color: gray;
+}
+
+.substitution-card > .card-section > input {
+ flex-grow: 1;
+ min-width: 0;
+}
+
+.substitution-card > * {
+ min-width: 0;
+}
+
+.substitution-card input {
+ padding-left: 2px;
+ border-color: var(--setting_background);
+}
+
+#new-sub-card {
+ display: flex;
+ justify-content: center;
+ background-color: var(--setting_background);
+ border-radius: 2px;
+ padding: 1px;
+ margin: 3px 0px;
+}
+
+.true-t {
+ display: none;
+}
+
+.true-t + label::before {
+ content: "edit_off";
+ color: gray;
+ margin-left: 3px;
+}
+
+.true-t:checked + label::before {
+ content: "edit";
+ color: white
+}
+
+
/*---------------------------------- Global ------------------------------------------------*/
.hidden {
display: none;
@@ -2553,30 +2651,33 @@ input[type='range'] {
/*Tooltip based on attribute*/
[tooltip] {
- cursor: pointer;
- display: inline-block;
- line-height: 1;
- position: relative;
+ cursor: pointer;
+ display: inline-block;
+ line-height: 1;
+ position: relative;
}
+
[tooltip]::after {
- background-color: rgba(51, 51, 51, 0.9);
- border-radius: 0.3rem;
- color: #fff;
- content: attr(tooltip);
- font-size: 1rem;
- font-size: 85%;
- font-weight: normal;
- line-height: 1.15rem;
- opacity: 0;
- padding: 0.25rem 0.5rem;
- position: absolute;
- text-align: center;
- text-transform: none;
- transition: opacity 0.2s;
- visibility: hidden;
- white-space: nowrap;
- z-index: 1;
+ background-color: rgba(51, 51, 51, 0.9);
+ border-radius: 0.3rem;
+ color: #fff;
+ content: attr(tooltip);
+ font-size: 1rem;
+ font-size: 85%;
+ font-weight: normal;
+ line-height: 1.15rem;
+ opacity: 0;
+ padding: 0.25rem 0.5rem;
+ position: absolute;
+ text-align: center;
+ text-transform: none;
+ transition: opacity 0.2s;
+ visibility: hidden;
+ white-space: nowrap;
+ z-index: 9999;
+ pointer-events: none;
}
+
@media (max-width: 767px) {
[tooltip].tooltip::before {
display: none;
diff --git a/static/koboldai.js b/static/koboldai.js
index 3045d568..82e5460d 100644
--- a/static/koboldai.js
+++ b/static/koboldai.js
@@ -33,6 +33,7 @@ socket.on("request_prompt_config", configurePrompt);
socket.on("log_message", function(data){process_log_message(data);});
socket.on("debug_message", function(data){console.log(data);});
socket.on("scratchpad_response", recieveScratchpadResponse);
+socket.on("scratchpad_response", recieveScratchpadResponse);
//socket.onAny(function(event_name, data) {console.log({"event": event_name, "class": data.classname, "data": data});});
var presets = {};
@@ -596,6 +597,9 @@ function var_changed(data) {
//Special Case for phrase biasing
} else if ((data.classname == 'story') && (data.name == 'biases')) {
do_biases(data);
+ //Special Case for substitutions
+ } else if ((data.classname == 'story') && (data.name == 'substitutions')) {
+ load_substitutions(data.value);
//Special Case for sample_order
} else if ((data.classname == 'model') && (data.name == 'sampler_order')) {
for (const [index, item] of data.value.entries()) {
@@ -2841,6 +2845,22 @@ function update_bias_slider_value(slider) {
slider.parentElement.parentElement.querySelector(".bias_slider_cur").textContent = slider.value;
}
+function distortColor(rgb) {
+ // rgb are 0..255, NOT NORMALIZED!!!!!!
+ const brightnessTamperAmplitude = 0.1;
+ const psuedoHue = 12;
+
+ let brightnessDistortion = Math.random() * (255 * brightnessTamperAmplitude);
+ rgb = rgb.map(x => x + brightnessDistortion);
+
+ // Cheap hack to imitate hue rotation
+ rgb = rgb.map(x => x += (Math.random() * psuedoHue * 2) - psuedoHue);
+
+ // Clamp and round
+ rgb = rgb.map(x => Math.round(Math.max(0, Math.min(255, x))));
+ return rgb;
+}
+
function update_context(data) {
$(".context-block").remove();
@@ -2858,7 +2878,6 @@ function update_context(data) {
}
for (const entry of data) {
- //console.log(entry);
let contextClass = "context-" + ({
soft_prompt: "sp",
prompt: "prompt",
@@ -2869,14 +2888,27 @@ function update_context(data) {
submit: 'submit'
}[entry.type]);
- let el = document.createElement("span");
- el.classList.add("context-block");
- el.classList.add(contextClass);
- el.innerText = entry.text;
- el.title = entry.tokens + " tokens";
+ let el = $e(
+ "span",
+ $el("#context-container"),
+ {classes: ["context-block", contextClass]}
+ );
- el.innerHTML = el.innerHTML.replaceAll("
", 'keyboard_return');
+ let rgb = window.getComputedStyle(el)["background-color"].match(/(\d+), (\d+), (\d+)/).slice(1, 4).map(Number);
+ for (const [tokenId, token] of entry.tokens) {
+ let tokenColor = distortColor(rgb);
+ tokenColor = "#" + (tokenColor.map((x) => x.toString(16)).join(""));
+
+ let tokenEl = $e("span", el, {
+ classes: ["context-token"],
+ "token-id": tokenId === -1 ? "Soft" : tokenId,
+ innerText: token,
+ "style.backgroundColor": tokenColor,
+ });
+
+ tokenEl.innerHTML = tokenEl.innerHTML.replaceAll("
", 'keyboard_return');
+ }
document.getElementById("context-container").appendChild(el);
switch (entry.type) {
@@ -3833,6 +3865,43 @@ async function loadKoboldData(data, filename) {
}
}
+function readLoreCard(file) {
+ // "naidata"
+ const magicNumber = new Uint8Array([0x6e, 0x61, 0x69, 0x64, 0x61, 0x74, 0x61]);
+
+ let filename = file.name;
+ let reader = new FileReader();
+ reader.readAsArrayBuffer(file);
+
+ reader.addEventListener("load", function() {
+ let bin = new Uint8Array(reader.result);
+
+ // naidata is prefixed with magic number
+ let offset = bin.findIndex(function(item, possibleIndex, array) {
+ for (let i=0;i 2000) {
+ alert("Some Substitution shenanigans are afoot; please send the developers your substitutions!");
+ throw Error("Substitution shenanigans!")
+ return;
+ }
+
+ let escape = true;
+ for (const c of substitutions) {
+ if (c.target === bareBonesTarget) continue;
+ if (!c.enabled) continue;
+
+ if (whatWeGot.includes(c.target)) {
+ whatWeGot = whatWeGot.replaceAll(c.target, c.substitution);
+ escape = false;
+ break;
+ }
+ }
+
+ if (escape) break;
+ }
+
+ return whatWeGot;
+ }
+
+ function getSubstitutionIndex(cardElement) {
+ for (const i in substitutions) {
+ if (substitutions[i].card === cardElement) {
+ return i
+ }
+ }
+
+ throw Error("Didn't find substitution!");
+ }
+
+ function getDuplicateCards(target) {
+ let duplicates = [];
+
+ for (const c of substitutions) {
+ if (c.target === target) duplicates.push(c.card);
+ }
+
+ console.log(duplicates)
+ return duplicates.length > 1 ? duplicates : [];
+ }
+
+ function makeCard(c) {
+ // How do we differentiate -- and ---? Convert stuff!
+
+ let card = $e("div", substitutionContainer, {classes: ["substitution-card"]});
+ let leftContainer = $e("div", card, {classes: ["card-section", "card-left"]});
+ let deleteIcon = $e("span", leftContainer, {classes: ["material-icons-outlined", "cursor"], innerText: "clear"});
+ let targetInput = $e("input", leftContainer, {classes: ["target"], value: c.target});
+ let rightContainer = $e("div", card, {classes: ["card-section"]});
+ let substitutionInput = $e("input", rightContainer, {classes: ["target"], value: c.substitution});
+
+ // HACK
+ let checkboxId = "sbcb" + Math.round(Math.random() * 9999).toString();
+
+ let enabledCheckbox = $e("input", rightContainer, {id: checkboxId, classes: ["true-t"], type: "checkbox", checked: c.enabled});
+ let initCheckTooltip = c.enabled ? "Enabled" : "Disabled";
+
+ // HACK: We don't use in-house tooltip as it's cut off by container :(
+ let enabledVisual = $e("label", rightContainer, {for: checkboxId, "title": initCheckTooltip, classes: ["material-icons-outlined"]});
+
+ targetInput.addEventListener("change", function() {
+ let card = this.parentElement.parentElement;
+ let i = getSubstitutionIndex(card);
+
+ substitutions[i].target = this.value;
+
+ // Don't do a full rebake
+ substitutions[i].trueTarget = getTrueTarget(this.value);
+
+ for (const duplicateCard of getDuplicateCards(this.value)) {
+ if (duplicateCard === card) continue;
+ console.log("DUPE", duplicateCard)
+ substitutions.splice(getSubstitutionIndex(duplicateCard), 1);
+ duplicateCard.remove();
+ }
+
+ rebuildCharMap();
+ updateSubstitutions();
+ });
+
+ substitutionInput.addEventListener("change", function() {
+ let card = this.parentElement.parentElement;
+ let i = getSubstitutionIndex(card);
+
+ substitutions[i].substitution = this.value;
+ // No rebaking at all is needed, that all hinges on target value; not edited here.
+ updateSubstitutions();
+ });
+
+ deleteIcon.addEventListener("click", function() {
+ let card = this.parentElement.parentElement;
+
+ // Find and remove from substitution array
+ substitutions.splice(getSubstitutionIndex(card), 1);
+ updateSubstitutions();
+ rebakeSubstitutions();
+ card.remove();
+ });
+
+ enabledCheckbox.addEventListener("change", function() {
+ let card = this.parentElement.parentElement;
+ let i = getSubstitutionIndex(card);
+ console.log(this.checked)
+
+ substitutions[i].enabled = this.checked;
+ enabledVisual.setAttribute("title", this.checked ? "Enabled" : "Disabled")
+ rebakeSubstitutions();
+ updateSubstitutions();
+ });
+
+ return card;
+ }
+
+ function updateSubstitutions() {
+ let subs = substitutions.map(x => ({target: x.target, substitution: x.substitution, trueTarget: x.trueTarget, enabled: x.enabled}));
+ socket.emit("substitution_update", subs);
+ }
+
+ function rebakeSubstitutions() {
+ for (const c of substitutions) {
+ c.trueTarget = getTrueTarget(c.target);
+ }
+ rebuildCharMap();
+ }
+
+ function rebuildCharMap() {
+ charMap = [];
+ for (const c of substitutions) {
+ if (!c.enabled) continue;
+ for (const char of c.target) {
+ if (!charMap.includes(char)) charMap.push(char)
+ }
+ }
+ }
+
+ const newCardButton = $el("#new-sub-card");
+ newCardButton.addEventListener("click", function() {
+ let c = {target: "", substitution: "", enabled: true}
+ substitutions.push(c);
+ c.card = makeCard(c);
+ newCardButton.scrollIntoView();
+ });
+
+ // Event handler on input
+ // TODO: Apply to all of gametext
+ const inputText = $el("#input_text");
+ inputText.addEventListener("keydown", function(event) {
+ if (event.ctrlKey) return;
+ if (event.ctrlKey) return;
+ if (!charMap.includes(event.key)) return;
+
+ let caretPosition = inputText.selectionStart;
+ // We don't have to worry about special keys due to charMap (hopefully)
+ let futureValue = inputText.value.slice(0, caretPosition) + event.key + inputText.value.slice(caretPosition);
+
+ for (const c of substitutions) {
+ if (!c.target) continue;
+ if (!c.enabled) continue;
+
+ let t = c.trueTarget;
+ let preCaretPosition = caretPosition - t.length + 1;
+ let bit = futureValue.slice(caretPosition - t.length + 1, caretPosition + 1)
+
+ if (bit === t) {
+ // We're doing it!!!!
+ event.preventDefault();
+
+ // Assemble the new text value
+ let before = inputText.value.slice(0, caretPosition - t.length + 1);
+ let after = inputText.value.slice(caretPosition);
+ let newText = before + c.substitution + after;
+
+ inputText.value = newText;
+
+ // Move cursor back after setting text
+ let sLength = c.substitution.length;
+ inputText.selectionStart = preCaretPosition + sLength;
+ inputText.selectionEnd = preCaretPosition + sLength;
+
+ break;
+ }
+ }
+ });
+
+ let firstLoad = true;
+
+ function load_substitutions(miniSubs) {
+ // HACK: Does the same "replace all on load" thing that WI does; tab
+ // support is broken and overall that kinda sucks. Would be nice to
+ // make a robust system for syncing multiple entries.
+
+ console.log("load", miniSubs)
+
+ $(".substitution-card").remove();
+ // we only get target, trueTarget, and such
+ for (const c of miniSubs) {
+ if (!c.trueTarget) c.trueTarget = getTrueTarget(c.target);
+ //if (!c.enabled) c.enabled = false;
+ c.card = makeCard(c);
+ }
+ substitutions = miniSubs;
+ rebuildCharMap();
+
+ // We build trueTarget on the client, and it's not initalized on the server because I'm lazy.
+ // May want to do that on the server in the future.
+ if (firstLoad) updateSubstitutions();
+ firstLoad = false;
+ }
+
+ return [load_substitutions];
+})();
+
/* -- Shortcuts -- */
document.addEventListener("keydown", function(event) {
diff --git a/templates/settings flyout.html b/templates/settings flyout.html
index 7fcabc70..b210a4a6 100644
--- a/templates/settings flyout.html
+++ b/templates/settings flyout.html
@@ -412,6 +412,27 @@
+
+
+
+
+
Automatically replaces phrases that you or the AI insert.
+
+
help_icon
+
+
+
+
+
+ add
+
+
+
+