diff --git a/.gitignore b/.gitignore
index 59605d83..15343072 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,4 +8,10 @@ stories/*
*.bak
miniconda3/*
*.settings
-__pycache__
\ No newline at end of file
+__pycache__
+
+# Ignore PyCharm project files.
+.idea
+
+# Ignore compiled Python files.
+*.pyc
diff --git a/aiserver.py b/aiserver.py
index 9b4c4c44..36b11bdc 100644
--- a/aiserver.py
+++ b/aiserver.py
@@ -10,6 +10,9 @@ import re
import tkinter as tk
from tkinter import messagebox
import json
+import collections
+from typing import Literal, Union
+
import requests
import html
import argparse
@@ -21,6 +24,7 @@ import fileops
import gensettings
from utils import debounce
import utils
+import structures
import breakmodel
#==================================================================#
@@ -76,7 +80,7 @@ class vars:
memory = "" # Text submitted to memory field
authornote = "" # Text submitted to Author's Note field
andepth = 3 # How far back in history to append author's note
- actions = [] # Array of actions submitted by user and AI
+ actions = structures.KoboldStoryRegister() # Actions submitted by user and AI
worldinfo = [] # Array of World Info key/value objects
badwords = [] # Array of str/chr values that should be removed from output
badwordsids = [] # Tokenized array of badwords
@@ -706,7 +710,6 @@ def get_message(msg):
vars.adventure = msg['data']
settingschanged()
refresh_settings()
- refresh_story()
elif(msg['cmd'] == 'importwi'):
wiimportrequest()
@@ -865,13 +868,13 @@ def actionsubmit(data, actionmode=0):
data = applyinputformatting(data)
# Store the result in the Action log
vars.actions.append(data)
-
+ update_story_chunk('last')
+
if(not vars.noai):
# Off to the tokenizer!
calcsubmit(data)
emit('from_server', {'cmd': 'scrolldown', 'data': ''}, broadcast=True)
else:
- refresh_story()
set_aibusy(0)
emit('from_server', {'cmd': 'scrolldown', 'data': ''}, broadcast=True)
@@ -888,9 +891,10 @@ def actionretry(data):
if(vars.gamestarted if vars.useprompt else len(vars.actions) > 0):
set_aibusy(1)
if(not vars.recentback and len(vars.actions) != 0 and len(vars.genseqs) == 0): # Don't pop if we're in the "Select sequence to keep" menu or if there are no non-prompt actions
+ last_key = vars.actions.get_last_key()
vars.actions.pop()
+ remove_story_chunk(last_key + 1)
vars.genseqs = []
- refresh_story()
calcsubmit('')
vars.recentback = False
elif(not vars.useprompt):
@@ -904,9 +908,10 @@ def actionback():
return
# Remove last index of actions and refresh game screen
if(len(vars.genseqs) == 0 and len(vars.actions) > 0):
+ last_key = vars.actions.get_last_key()
vars.actions.pop()
vars.recentback = True
- refresh_story()
+ remove_story_chunk(last_key + 1)
elif(len(vars.genseqs) == 0):
emit('from_server', {'cmd': 'errmsg', 'data': "Cannot delete the prompt."})
else:
@@ -977,11 +982,13 @@ def calcsubmit(txt):
forceanote = True
# Get most recent action tokens up to our budget
- for n in range(actionlen):
+ n = 0
+ for key in reversed(vars.actions):
+ chunk = vars.actions[key]
if(budget <= 0):
break
- acttkns = tokenizer.encode(vars.actions[(-1-n)])
+ acttkns = tokenizer.encode(chunk)
tknlen = len(acttkns)
if(tknlen < budget):
tokens = acttkns + tokens
@@ -997,6 +1004,7 @@ def calcsubmit(txt):
if(anotetxt != ""):
tokens = anotetkns + tokens # A.N. len already taken from bdgt
anoteadded = True
+ n += 1
# If we're not using the prompt every time and there's still budget left,
# add some prompt.
@@ -1051,17 +1059,19 @@ def calcsubmit(txt):
subtxt = ""
prompt = vars.prompt
- for n in range(actionlen):
+ n = 0
+ for key in reversed(vars.actions):
+ chunk = vars.actions[key]
if(budget <= 0):
break
- actlen = len(vars.actions[(-1-n)])
+ actlen = len(chunk)
if(actlen < budget):
- subtxt = vars.actions[(-1-n)] + subtxt
+ subtxt = chunk + subtxt
budget -= actlen
else:
count = budget * -1
- subtxt = vars.actions[(-1-n)][count:] + subtxt
+ subtxt = chunk[count:] + subtxt
budget = 0
break
@@ -1078,6 +1088,7 @@ def calcsubmit(txt):
if(anotetxt != ""):
subtxt = anotetxt + subtxt # A.N. len already taken from bdgt
anoteadded = True
+ n += 1
# Did we get to add the A.N.? If not, do it here
if(anotetxt != ""):
@@ -1169,8 +1180,8 @@ def genresult(genout):
# Add formatted text to Actions array and refresh the game screen
vars.actions.append(genout)
- refresh_story()
- emit('from_server', {'cmd': 'texteffect', 'data': len(vars.actions)}, broadcast=True)
+ update_story_chunk('last')
+ emit('from_server', {'cmd': 'texteffect', 'data': vars.actions.get_last_key() if len(vars.actions) else 0}, broadcast=True)
#==================================================================#
# Send generator sequences to the UI for selection
@@ -1188,9 +1199,6 @@ def genselect(genout):
# Send sequences to UI for selection
emit('from_server', {'cmd': 'genseqs', 'data': genout}, broadcast=True)
-
- # Refresh story for any input text
- refresh_story()
#==================================================================#
# Send selected sequence to action log and refresh UI
@@ -1199,8 +1207,8 @@ def selectsequence(n):
if(len(vars.genseqs) == 0):
return
vars.actions.append(vars.genseqs[int(n)]["generated_text"])
- refresh_story()
- emit('from_server', {'cmd': 'texteffect', 'data': len(vars.actions)}, broadcast=True)
+ update_story_chunk('last')
+ emit('from_server', {'cmd': 'texteffect', 'data': vars.actions.get_last_key() if len(vars.actions) else 0}, broadcast=True)
emit('from_server', {'cmd': 'hidegenseqs', 'data': ''}, broadcast=True)
vars.genseqs = []
@@ -1259,7 +1267,7 @@ def sendtocolab(txt, min, max):
# Add formatted text to Actions array and refresh the game screen
#vars.actions.append(genout)
#refresh_story()
- #emit('from_server', {'cmd': 'texteffect', 'data': len(vars.actions)})
+ #emit('from_server', {'cmd': 'texteffect', 'data': vars.actions.get_last_key() if len(vars.actions) else 0})
set_aibusy(0)
else:
@@ -1330,13 +1338,51 @@ def applyoutputformatting(txt):
# Sends the current story content to the Game Screen
#==================================================================#
def refresh_story():
- text_parts = ['', html.escape(vars.prompt), '']
- for idx, item in enumerate(vars.actions, start=1):
- if vars.adventure: # Add special formatting to adventure actions
- item = vars.acregex_ui.sub('\\1', html.escape(item))
- text_parts.extend(('', item, ''))
+ text_parts = ['', html.escape(vars.prompt), '']
+ for idx in vars.actions:
+ item = vars.actions[idx]
+ idx += 1
+ item = html.escape(item)
+ item = vars.acregex_ui.sub('\\1', item) # Add special formatting to adventure actions
+ text_parts.extend(('', item, ''))
emit('from_server', {'cmd': 'updatescreen', 'gamestarted': vars.gamestarted, 'data': formatforhtml(''.join(text_parts))}, broadcast=True)
+
+#==================================================================#
+# Signals the Game Screen to update one of the chunks
+#==================================================================#
+def update_story_chunk(idx: Union[int, Literal['last']]):
+ if idx == 'last':
+ if len(vars.actions) <= 1:
+ # In this case, we are better off just refreshing the whole thing as the
+ # prompt might not have been shown yet (with a "Generating story..."
+ # message instead).
+ refresh_story()
+ return
+
+ idx = (vars.actions.get_last_key() if len(vars.actions) else 0) + 1
+
+ if idx == 0:
+ text = vars.prompt
+ else:
+ # Actions are 0 based, but in chunks 0 is the prompt.
+ # So the chunk index is one more than the corresponding action index.
+ text = vars.actions[idx - 1]
+
+ item = html.escape(text)
+ item = vars.acregex_ui.sub('\\1', item) # Add special formatting to adventure actions
+
+ chunk_text = f'{formatforhtml(item)}'
+ emit('from_server', {'cmd': 'updatechunk', 'data': {'index': idx, 'html': chunk_text, 'last': (idx == (vars.actions.get_last_key() if len(vars.actions) else 0))}}, broadcast=True)
+
+
+#==================================================================#
+# Signals the Game Screen to remove one of the chunks
+#==================================================================#
+def remove_story_chunk(idx: int):
+ emit('from_server', {'cmd': 'removechunk', 'data': idx}, broadcast=True)
+
+
#==================================================================#
# Sends the current generator settings to the Game Menu
#==================================================================#
@@ -1405,7 +1451,7 @@ def editsubmit(data):
vars.actions[vars.editln-1] = data
vars.mode = "play"
- refresh_story()
+ update_story_chunk(vars.editln)
emit('from_server', {'cmd': 'texteffect', 'data': vars.editln}, broadcast=True)
emit('from_server', {'cmd': 'editmode', 'data': 'false'})
@@ -1420,7 +1466,7 @@ def deleterequest():
else:
del vars.actions[vars.editln-1]
vars.mode = "play"
- refresh_story()
+ remove_story_chunk(vars.editln)
emit('from_server', {'cmd': 'editmode', 'data': 'false'})
#==================================================================#
@@ -1433,7 +1479,7 @@ def inlineedit(chunk, data):
else:
vars.actions[chunk-1] = data
- refresh_story()
+ update_story_chunk(chunk)
emit('from_server', {'cmd': 'texteffect', 'data': chunk}, broadcast=True)
emit('from_server', {'cmd': 'editmode', 'data': 'false'}, broadcast=True)
@@ -1445,12 +1491,12 @@ def inlinedelete(chunk):
# Don't delete prompt
if(chunk == 0):
# Send error message
- refresh_story()
+ update_story_chunk(chunk)
emit('from_server', {'cmd': 'errmsg', 'data': "Cannot delete the prompt."})
emit('from_server', {'cmd': 'editmode', 'data': 'false'}, broadcast=True)
else:
del vars.actions[chunk-1]
- refresh_story()
+ remove_story_chunk(chunk)
emit('from_server', {'cmd': 'editmode', 'data': 'false'}, broadcast=True)
#==================================================================#
@@ -1580,10 +1626,19 @@ def checkworldinfo(txt):
txt = ""
depth += 1
+ if(ln > 0):
+ chunks = collections.deque()
+ i = 0
+ for key in reversed(vars.actions):
+ chunk = vars.actions[key]
+ chunks.appendleft(chunk)
+ if(i == depth):
+ break
+
if(ln >= depth):
- txt = "".join(vars.actions[(depth*-1):])
+ txt = "".join(chunks)
elif(ln > 0):
- txt = vars.prompt + "".join(vars.actions[(depth*-1):])
+ txt = vars.prompt + "".join(chunks)
elif(ln == 0):
txt = vars.prompt
@@ -1679,8 +1734,8 @@ def ikrequest(txt):
genout = req.json()["data"]["text"]
print("{0}{1}{2}".format(colors.CYAN, genout, colors.END))
vars.actions.append(genout)
- refresh_story()
- emit('from_server', {'cmd': 'texteffect', 'data': len(vars.actions)}, broadcast=True)
+ update_story_chunk('last')
+ emit('from_server', {'cmd': 'texteffect', 'data': vars.actions.get_last_key() if len(vars.actions) else 0}, broadcast=True)
set_aibusy(0)
else:
@@ -1729,8 +1784,8 @@ def oairequest(txt, min, max):
genout = req.json()["choices"][0]["text"]
print("{0}{1}{2}".format(colors.CYAN, genout, colors.END))
vars.actions.append(genout)
- refresh_story()
- emit('from_server', {'cmd': 'texteffect', 'data': len(vars.actions)}, broadcast=True)
+ update_story_chunk('last')
+ emit('from_server', {'cmd': 'texteffect', 'data': vars.actions.get_last_key() if len(vars.actions) else 0}, broadcast=True)
set_aibusy(0)
else:
@@ -1808,7 +1863,7 @@ def saveRequest(savpath):
js["prompt"] = vars.prompt
js["memory"] = vars.memory
js["authorsnote"] = vars.authornote
- js["actions"] = vars.actions
+ js["actions"] = tuple(vars.actions.values())
js["worldinfo"] = []
# Extract only the important bits of WI
@@ -1860,10 +1915,14 @@ def loadRequest(loadpath):
vars.gamestarted = js["gamestarted"]
vars.prompt = js["prompt"]
vars.memory = js["memory"]
- vars.actions = js["actions"]
vars.worldinfo = []
vars.lastact = ""
vars.lastctx = ""
+
+ del vars.actions
+ vars.actions = structures.KoboldStoryRegister()
+ for s in js["actions"]:
+ vars.actions.append(s)
# Try not to break older save files
if("authorsnote" in js):
@@ -1973,7 +2032,7 @@ def importgame():
vars.prompt = ""
vars.memory = ref["memory"]
vars.authornote = ref["authorsNote"] if type(ref["authorsNote"]) is str else ""
- vars.actions = []
+ vars.actions = structures.KoboldStoryRegister()
vars.worldinfo = []
vars.lastact = ""
vars.lastctx = ""
@@ -2033,7 +2092,7 @@ def importAidgRequest(id):
vars.prompt = js["promptContent"]
vars.memory = js["memory"]
vars.authornote = js["authorsNote"]
- vars.actions = []
+ vars.actions = structures.KoboldStoryRegister()
vars.worldinfo = []
vars.lastact = ""
vars.lastctx = ""
@@ -2101,7 +2160,7 @@ def newGameRequest():
vars.gamestarted = False
vars.prompt = ""
vars.memory = ""
- vars.actions = []
+ vars.actions = structures.KoboldStoryRegister()
vars.authornote = ""
vars.worldinfo = []
diff --git a/static/application.js b/static/application.js
index 5fda5b37..185c7448 100644
--- a/static/application.js
+++ b/static/application.js
@@ -643,6 +643,11 @@ function setmodevisibility(state) {
function setadventure(state) {
adventure = state;
+ if(state) {
+ game_text.addClass("adventure");
+ } else {
+ game_text.removeClass("adventure");
+ }
if(!memorymode){
setmodevisibility(state);
}
@@ -758,19 +763,12 @@ function submitEditedChunk(event) {
return;
}
- show([$('#curtain')]);
- setTimeout(function () {
- if(document.activeElement.tagName == "CHUNK") {
- document.activeElement.blur();
- }
- }, 5);
-
chunk = current_editing_chunk;
current_editing_chunk = null;
// Submit the edited chunk if it's not empty, otherwise delete it
if(chunk.innerText.length) {
- socket.send({'cmd': 'inlineedit', 'chunk': chunk.getAttribute("n"), 'data': chunk.innerText});
+ socket.send({'cmd': 'inlineedit', 'chunk': chunk.getAttribute("n"), 'data': chunk.innerText.replace(/\u00a0/g, " ")});
} else {
socket.send({'cmd': 'inlinedelete', 'data': chunk.getAttribute("n")});
}
@@ -841,11 +839,11 @@ $(document).ready(function(){
seqselmenu = $("#seqselmenu");
seqselcontents = $("#seqselcontents");
- // Connect to SocketIO server
- socket = io.connect(window.document.origin);
+ // Connect to SocketIO server
+ socket = io.connect(window.document.origin);
socket.on('from_server', function(msg) {
- if(msg.cmd == "connected") {
+ if(msg.cmd == "connected") {
// Connected to Server Actions
connected = true;
connect_status.html("Connected to KoboldAI Process!");
@@ -876,9 +874,7 @@ $(document).ready(function(){
}
game_text.html(msg.data);
// Make content editable if need be
- $("chunk").attr('tabindex', -1)
$('chunk').attr('contenteditable', allowedit);
- hide([$('#curtain')]);
// Scroll to bottom of text
if(newly_loaded) {
setTimeout(function () {
@@ -891,6 +887,33 @@ $(document).ready(function(){
setTimeout(function () {
$('#gamescreen').animate({scrollTop: $('#gamescreen').prop('scrollHeight')}, 1000);
}, 5);
+ } else if(msg.cmd == "updatechunk") {
+ hideMessage();
+ const {index, html, last} = msg.data;
+ const existingChunk = game_text.children(`#n${index}`)
+ const newChunk = $(html);
+ if (existingChunk.length > 0) {
+ // Update existing chunk
+ existingChunk.before(newChunk);
+ existingChunk.remove();
+ } else {
+ // Append at the end
+ game_text.append(newChunk);
+ }
+ newChunk.attr('contenteditable', allowedit);
+ hide([$('#curtain')]);
+ if(last) {
+ // Scroll to bottom of text if it's the last element
+ setTimeout(function () {
+ $('#gamescreen').animate({scrollTop: $('#gamescreen').prop('scrollHeight')}, 1000);
+ }, 5);
+ }
+ } else if(msg.cmd == "removechunk") {
+ hideMessage();
+ let index = msg.data;
+ // Remove the chunk
+ game_text.children(`#n${index}`).remove()
+ hide([$('#curtain')]);
} else if(msg.cmd == "setgamestate") {
// Enable or Disable buttons
if(msg.data == "ready") {
@@ -1088,7 +1111,7 @@ $(document).ready(function(){
} else if(msg.cmd == "runs_remotely") {
hide([button_loadfrfile, button_savetofile, button_import, button_importwi]);
}
- });
+ });
socket.on('disconnect', function() {
connected = false;
diff --git a/static/custom.css b/static/custom.css
index 58bfc37c..19349702 100644
--- a/static/custom.css
+++ b/static/custom.css
@@ -6,14 +6,14 @@ chunk {
color: #ffffff;
}
-action {
+#gametext.adventure action {
color: #9ff7fa;
font-weight: bold;
}
chunk[contenteditable="true"]:focus, chunk[contenteditable="true"]:focus * {
- color: #cdf;
- font-weight: normal;
+ color: #cdf !important;
+ font-weight: normal !important;
}
chunk, chunk * {
@@ -355,8 +355,8 @@ chunk, chunk * {
.colorfade, .colorfade * {
-moz-transition:color 1s ease-in;
- -o-transition:color 1s ease-in;
- -webkit-transition:color 1s ease-in;
+ -o-transition:color 1s ease-in;
+ -webkit-transition:color 1s ease-in;
transition:color 1s ease-in;
}
@@ -398,7 +398,7 @@ chunk, chunk * {
}
.edit-flash, .edit-flash * {
- color: #3bf723;
+ color: #3bf723 !important;
}
.flex {
@@ -443,21 +443,21 @@ chunk, chunk * {
}
.helpicon {
- display: inline-block;
- font-family: sans-serif;
- font-weight: bold;
- text-align: center;
- width: 2.2ex;
- height: 2.4ex;
- font-size: 1.4ex;
- line-height: 1.8ex;
- border-radius: 1.2ex;
- margin-right: 4px;
- padding: 1px;
- color: #295071;
- background: #ffffff;
- border: 1px solid white;
- text-decoration: none;
+ display: inline-block;
+ font-family: sans-serif;
+ font-weight: bold;
+ text-align: center;
+ width: 2.2ex;
+ height: 2.4ex;
+ font-size: 1.4ex;
+ line-height: 1.8ex;
+ border-radius: 1.2ex;
+ margin-right: 4px;
+ padding: 1px;
+ color: #295071;
+ background: #ffffff;
+ border: 1px solid white;
+ text-decoration: none;
}
.helpicon:hover {
@@ -569,8 +569,8 @@ chunk, chunk * {
color: #ffffff;
-moz-transition: background-color 0.25s ease-in;
- -o-transition: background-color 0.25s ease-in;
- -webkit-transition: background-color 0.25s ease-in;
+ -o-transition: background-color 0.25s ease-in;
+ -webkit-transition: background-color 0.25s ease-in;
transition: background-color 0.25s ease-in;
}
@@ -580,12 +580,12 @@ chunk, chunk * {
}
.navbar .navbar-nav .nav-link:hover {
- border-radius: 5px;
+ border-radius: 5px;
background-color: #98bcdb;
}
.navbar .navbar-nav .nav-link:focus {
- border-radius: 5px;
+ border-radius: 5px;
background-color: #98bcdb;
}
@@ -651,8 +651,8 @@ chunk, chunk * {
color: #ffffff;
-moz-transition: background-color 0.25s ease-in;
- -o-transition: background-color 0.25s ease-in;
- -webkit-transition: background-color 0.25s ease-in;
+ -o-transition: background-color 0.25s ease-in;
+ -webkit-transition: background-color 0.25s ease-in;
transition: background-color 0.25s ease-in;
}
@@ -702,8 +702,8 @@ chunk, chunk * {
padding: 5px;
color: #ffffff;
-moz-transition: all 0.15s ease-in;
- -o-transition: all 0.15s ease-in;
- -webkit-transition: all 0.15s ease-in;
+ -o-transition: all 0.15s ease-in;
+ -webkit-transition: all 0.15s ease-in;
transition: all 0.15s ease-in;
}
diff --git a/structures.py b/structures.py
new file mode 100644
index 00000000..287f92c1
--- /dev/null
+++ b/structures.py
@@ -0,0 +1,40 @@
+import collections
+from typing import Iterable, Tuple
+
+
+class KoboldStoryRegister(collections.OrderedDict):
+ '''
+ Complexity-optimized class for keeping track of story chunks
+ '''
+
+ def __init__(self, sequence: Iterable[Tuple[int, str]] = ()):
+ super().__init__(sequence)
+ self.__next_id: int = len(sequence)
+
+ def append(self, v: str) -> None:
+ self[self.__next_id] = v
+ self.increment_id()
+
+ def pop(self) -> str:
+ return self.popitem()[1]
+
+ def get_first_key(self) -> int:
+ return next(iter(self))
+
+ def get_last_key(self) -> int:
+ return next(reversed(self))
+
+ def __getitem__(self, k: int) -> str:
+ return super().__getitem__(k)
+
+ def __setitem__(self, k: int, v: str) -> None:
+ return super().__setitem__(k, v)
+
+ def increment_id(self) -> None:
+ self.__next_id += 1
+
+ def get_next_id(self) -> int:
+ return self.__next_id
+
+ def set_next_id(self, x: int) -> None:
+ self.__next_id = x
diff --git a/templates/index.html b/templates/index.html
index 6f4a4248..085d9245 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -247,7 +247,7 @@
Unsaved data will be lost.
Below you can input a genre suggestion for the AI to loosely base the story on (For example Horror or Cowboy).
-