Merge pull request #15 from VE-FORBRYDERNE/story-manager

In-browser story management patch
This commit is contained in:
henk717 2021-09-02 11:53:24 +02:00 committed by GitHub
commit aace0d058f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 347 additions and 75 deletions

View File

@ -26,7 +26,6 @@ import gensettings
from utils import debounce from utils import debounce
import utils import utils
import structures import structures
import breakmodel
#==================================================================# #==================================================================#
# Variables & Storage # Variables & Storage
@ -111,6 +110,8 @@ class vars:
useprompt = True # Whether to send the full prompt with every submit action useprompt = True # Whether to send the full prompt with every submit action
breakmodel = False # For GPU users, whether to use both system RAM and VRAM to conserve VRAM while offering speedup compared to CPU-only breakmodel = False # For GPU users, whether to use both system RAM and VRAM to conserve VRAM while offering speedup compared to CPU-only
bmsupported = False # Whether the breakmodel option is supported (GPT-Neo/GPT-J only, currently) bmsupported = False # Whether the breakmodel option is supported (GPT-Neo/GPT-J only, currently)
smandelete = False # Whether stories can be deleted from inside the browser
smanrename = False # Whether stories can be renamed from inside the browser
acregex_ai = re.compile(r'\n* *>(.|\n)*') # Pattern for matching adventure actions from the AI so we can remove them acregex_ai = re.compile(r'\n* *>(.|\n)*') # Pattern for matching adventure actions from the AI so we can remove them
acregex_ui = re.compile(r'^ *(>.*)$', re.MULTILINE) # Pattern for matching actions in the HTML-escaped story so we can apply colouring, etc (make sure to encase part to format in parentheses) acregex_ui = re.compile(r'^ *(>.*)$', re.MULTILINE) # Pattern for matching actions in the HTML-escaped story so we can apply colouring, etc (make sure to encase part to format in parentheses)
actionmode = 1 actionmode = 1
@ -173,12 +174,17 @@ parser.add_argument("--path", help="Specify the Path for local models (For model
parser.add_argument("--cpu", action='store_true', help="By default unattended launches are on the GPU use this option to force CPU usage.") parser.add_argument("--cpu", action='store_true', help="By default unattended launches are on the GPU use this option to force CPU usage.")
parser.add_argument("--breakmodel", action='store_true', help="For models that support GPU-CPU hybrid generation, use this feature instead of GPU or CPU generation") parser.add_argument("--breakmodel", action='store_true', help="For models that support GPU-CPU hybrid generation, use this feature instead of GPU or CPU generation")
parser.add_argument("--breakmodel_layers", type=int, help="Specify the number of layers to commit to system RAM if --breakmodel is used") parser.add_argument("--breakmodel_layers", type=int, help="Specify the number of layers to commit to system RAM if --breakmodel is used")
parser.add_argument("--override_delete", action='store_true', help="Deleting stories from inside the browser is disabled if you are using --remote and enabled otherwise. Using this option will instead allow deleting stories if using --remote and prevent deleting stories otherwise.")
parser.add_argument("--override_rename", action='store_true', help="Renaming stories from inside the browser is disabled if you are using --remote and enabled otherwise. Using this option will instead allow renaming stories if using --remote and prevent renaming stories otherwise.")
args = parser.parse_args() args = parser.parse_args()
vars.model = args.model; vars.model = args.model;
if args.remote: if args.remote:
vars.remote = True; vars.remote = True;
vars.smandelete = vars.remote == args.override_delete
vars.smanrename = vars.remote == args.override_rename
# Select a model to run # Select a model to run
if args.model: if args.model:
print("Welcome to KoboldAI!\nYou have selected the following Model:", vars.model) print("Welcome to KoboldAI!\nYou have selected the following Model:", vars.model)
@ -363,7 +369,7 @@ log.setLevel(logging.ERROR)
# Start flask & SocketIO # Start flask & SocketIO
print("{0}Initializing Flask... {1}".format(colors.PURPLE, colors.END), end="") print("{0}Initializing Flask... {1}".format(colors.PURPLE, colors.END), end="")
from flask import Flask, render_template from flask import Flask, render_template, Response, request
from flask_socketio import SocketIO, emit from flask_socketio import SocketIO, emit
app = Flask(__name__) app = Flask(__name__)
app.config['SECRET KEY'] = 'secret!' app.config['SECRET KEY'] = 'secret!'
@ -385,6 +391,7 @@ if(not vars.model in ["InferKit", "Colab", "OAI", "ReadOnly"]):
if(vars.usegpu): if(vars.usegpu):
generator = pipeline('text-generation', model=model, tokenizer=tokenizer, device=0) generator = pipeline('text-generation', model=model, tokenizer=tokenizer, device=0)
elif(vars.breakmodel): # Use both RAM and VRAM (breakmodel) elif(vars.breakmodel): # Use both RAM and VRAM (breakmodel)
import breakmodel
n_layers = model.config.num_layers n_layers = model.config.num_layers
breakmodel.total_blocks = n_layers breakmodel.total_blocks = n_layers
model.half().to('cpu') model.half().to('cpu')
@ -435,6 +442,7 @@ if(not vars.model in ["InferKit", "Colab", "OAI", "ReadOnly"]):
if(vars.usegpu): if(vars.usegpu):
generator = pipeline('text-generation', model=vars.model, device=0) generator = pipeline('text-generation', model=vars.model, device=0)
elif(vars.breakmodel): # Use both RAM and VRAM (breakmodel) elif(vars.breakmodel): # Use both RAM and VRAM (breakmodel)
import breakmodel
model = AutoModel.from_pretrained(vars.model) model = AutoModel.from_pretrained(vars.model)
n_layers = model.config.num_layers n_layers = model.config.num_layers
breakmodel.total_blocks = n_layers breakmodel.total_blocks = n_layers
@ -494,9 +502,17 @@ def index():
return render_template('index.html') return render_template('index.html')
@app.route('/download') @app.route('/download')
def download(): def download():
# Leave Edit/Memory mode before continuing save_format = request.args.get("format", "json").strip().lower()
exitModes()
if(save_format == "plaintext"):
txt = vars.prompt + "".join(vars.actions.values())
save = Response(txt)
filename = path.basename(vars.savedir)
if filename[-5:] == ".json":
filename = filename[:-5]
save.headers.set('Content-Disposition', 'attachment', filename='%s.txt' % filename)
return(save)
# Build json to write # Build json to write
js = {} js = {}
js["gamestarted"] = vars.gamestarted js["gamestarted"] = vars.gamestarted
@ -516,8 +532,12 @@ def download():
"selective": wi["selective"], "selective": wi["selective"],
"constant": wi["constant"] "constant": wi["constant"]
}) })
save = flask.Response(json.dumps(js, indent=3))
save.headers.set('Content-Disposition', 'attachment', filename='%s.json' % path.basename(vars.savedir)) save = Response(json.dumps(js, indent=3))
filename = path.basename(vars.savedir)
if filename[-5:] == ".json":
filename = filename[:-5]
save.headers.set('Content-Disposition', 'attachment', filename='%s.json' % filename)
return(save) return(save)
#============================ METHODS =============================# #============================ METHODS =============================#
@ -528,7 +548,7 @@ def download():
@socketio.on('connect') @socketio.on('connect')
def do_connect(): def do_connect():
print("{0}Client connected!{1}".format(colors.GREEN, colors.END)) print("{0}Client connected!{1}".format(colors.GREEN, colors.END))
emit('from_server', {'cmd': 'connected'}) emit('from_server', {'cmd': 'connected', 'smandelete': vars.smandelete, 'smanrename': vars.smanrename})
if(vars.remote): if(vars.remote):
emit('from_server', {'cmd': 'runs_remotely'}) emit('from_server', {'cmd': 'runs_remotely'})
@ -597,11 +617,11 @@ def get_message(msg):
deleterequest() deleterequest()
elif(msg['cmd'] == 'memory'): elif(msg['cmd'] == 'memory'):
togglememorymode() togglememorymode()
elif(msg['cmd'] == 'savetofile'): elif(not vars.remote and msg['cmd'] == 'savetofile'):
savetofile() savetofile()
elif(msg['cmd'] == 'loadfromfile'): elif(not vars.remote and msg['cmd'] == 'loadfromfile'):
loadfromfile() loadfromfile()
elif(msg['cmd'] == 'import'): elif(not vars.remote and msg['cmd'] == 'import'):
importRequest() importRequest()
elif(msg['cmd'] == 'newgame'): elif(msg['cmd'] == 'newgame'):
newGameRequest() newGameRequest()
@ -714,7 +734,11 @@ def get_message(msg):
elif(msg['cmd'] == 'loadselect'): elif(msg['cmd'] == 'loadselect'):
vars.loadselect = msg["data"] vars.loadselect = msg["data"]
elif(msg['cmd'] == 'loadrequest'): elif(msg['cmd'] == 'loadrequest'):
loadRequest(getcwd()+"/stories/"+vars.loadselect+".json") loadRequest(fileops.storypath(vars.loadselect))
elif(msg['cmd'] == 'deletestory'):
deletesave(msg['data'])
elif(msg['cmd'] == 'renamestory'):
renamesave(msg['data'], msg['newname'])
elif(msg['cmd'] == 'clearoverwrite'): elif(msg['cmd'] == 'clearoverwrite'):
vars.svowname = "" vars.svowname = ""
vars.saveow = False vars.saveow = False
@ -738,7 +762,7 @@ def get_message(msg):
vars.adventure = msg['data'] vars.adventure = msg['data']
settingschanged() settingschanged()
refresh_settings() refresh_settings()
elif(msg['cmd'] == 'importwi'): elif(not vars.remote and msg['cmd'] == 'importwi'):
wiimportrequest() wiimportrequest()
#==================================================================# #==================================================================#
@ -1848,16 +1872,62 @@ def saveas(name):
name = utils.cleanfilename(name) name = utils.cleanfilename(name)
if(not fileops.saveexists(name) or (vars.saveow and vars.svowname == name)): if(not fileops.saveexists(name) or (vars.saveow and vars.svowname == name)):
# All clear to save # All clear to save
saveRequest(getcwd()+"/stories/"+name+".json") e = saveRequest(fileops.storypath(name))
emit('from_server', {'cmd': 'hidesaveas', 'data': ''})
vars.saveow = False vars.saveow = False
vars.svowname = "" vars.svowname = ""
if(e is None):
emit('from_server', {'cmd': 'hidesaveas', 'data': ''})
else:
print("{0}{1}{2}".format(colors.RED, str(e), colors.END))
emit('from_server', {'cmd': 'popuperror', 'data': str(e)})
else: else:
# File exists, prompt for overwrite # File exists, prompt for overwrite
vars.saveow = True vars.saveow = True
vars.svowname = name vars.svowname = name
emit('from_server', {'cmd': 'askforoverwrite', 'data': ''}) emit('from_server', {'cmd': 'askforoverwrite', 'data': ''})
#==================================================================#
# Launch in-browser story-delete prompt
#==================================================================#
def deletesave(name):
name = utils.cleanfilename(name)
e = fileops.deletesave(name)
if(e is None):
if(vars.smandelete):
emit('from_server', {'cmd': 'hidepopupdelete', 'data': ''})
getloadlist()
else:
emit('from_server', {'cmd': 'popuperror', 'data': "The server denied your request to delete this story"})
else:
print("{0}{1}{2}".format(colors.RED, str(e), colors.END))
emit('from_server', {'cmd': 'popuperror', 'data': str(e)})
#==================================================================#
# Launch in-browser story-rename prompt
#==================================================================#
def renamesave(name, newname):
# Check if filename exists already
name = utils.cleanfilename(name)
newname = utils.cleanfilename(newname)
if(not fileops.saveexists(newname) or name == newname or (vars.saveow and vars.svowname == newname)):
e = fileops.renamesave(name, newname)
vars.saveow = False
vars.svowname = ""
if(e is None):
if(vars.smanrename):
emit('from_server', {'cmd': 'hidepopuprename', 'data': ''})
getloadlist()
else:
emit('from_server', {'cmd': 'popuperror', 'data': "The server denied your request to rename this story"})
else:
print("{0}{1}{2}".format(colors.RED, str(e), colors.END))
emit('from_server', {'cmd': 'popuperror', 'data': str(e)})
else:
# File exists, prompt for overwrite
vars.saveow = True
vars.svowname = newname
emit('from_server', {'cmd': 'askforoverwrite', 'data': ''})
#==================================================================# #==================================================================#
# Save the currently running story # Save the currently running story
#==================================================================# #==================================================================#
@ -1906,33 +1976,30 @@ def saveRequest(savpath):
"constant": wi["constant"] "constant": wi["constant"]
}) })
ln = len(vars.actions) txt = vars.prompt + "".join(vars.actions.values())
if(ln > 0):
chunks = collections.deque()
i = 0
for key in reversed(vars.actions):
chunk = vars.actions[key]
chunks.appendleft(chunk)
i += 1
if(ln > 0):
txt = vars.prompt + "".join(chunks)
elif(ln == 0):
txt = vars.prompt
# Write it # Write it
file = open(savpath, "w") try:
file = open(savpath, "w")
except Exception as e:
return e
try: try:
file.write(json.dumps(js, indent=3)) file.write(json.dumps(js, indent=3))
finally: except Exception as e:
file.close() file.close()
return e
file = open(txtpath, "w") file.close()
try:
file = open(txtpath, "w")
except Exception as e:
return e
try: try:
file.write(txt) file.write(txt)
finally: except Exception as e:
file.close() file.close()
return e
file.close()
print("{0}Story saved to {1}!{2}".format(colors.GREEN, path.basename(savpath), colors.END)) print("{0}Story saved to {1}!{2}".format(colors.GREEN, path.basename(savpath), colors.END))

View File

@ -1,6 +1,7 @@
import tkinter as tk import tkinter as tk
from tkinter import filedialog from tkinter import filedialog
from os import getcwd, listdir, path from os import getcwd, listdir, path
import os
import json import json
#==================================================================# #==================================================================#
@ -54,6 +55,12 @@ def getdirpath(dir, title):
else: else:
return None return None
#==================================================================#
# Returns the path (as a string) to the given story by its name
#==================================================================#
def storypath(name):
return path.join(path.dirname(path.realpath(__file__)), "stories", name + ".json")
#==================================================================# #==================================================================#
# Returns an array of dicts containing story files in /stories # Returns an array of dicts containing story files in /stories
#==================================================================# #==================================================================#
@ -83,4 +90,22 @@ def getstoryfiles():
# Returns True if json file exists with requested save name # Returns True if json file exists with requested save name
#==================================================================# #==================================================================#
def saveexists(name): def saveexists(name):
return path.exists(path.dirname(path.realpath(__file__))+"/stories/"+name+".json") return path.exists(storypath(name))
#==================================================================#
# Delete save file by name; returns None if successful, or the exception if not
#==================================================================#
def deletesave(name):
try:
os.remove(storypath(name))
except Exception as e:
return e
#==================================================================#
# Rename save file; returns None if successful, or the exception if not
#==================================================================#
def renamesave(name, new_name):
try:
os.replace(storypath(name), storypath(new_name))
except Exception as e:
return e

View File

@ -49,7 +49,6 @@ var saveasinput;
var topic; var topic;
var saveas_accept; var saveas_accept;
var saveas_close; var saveas_close;
var saveasoverwrite;
var loadpopup; var loadpopup;
var loadcontent; var loadcontent;
var load_accept; var load_accept;
@ -70,6 +69,8 @@ var connected = false;
var newly_loaded = true; var newly_loaded = true;
var current_editing_chunk = null; var current_editing_chunk = null;
var chunk_conflict = false; var chunk_conflict = false;
var sman_allow_delete = false;
var sman_allow_rename = false;
// Key states // Key states
var shift_down = false; var shift_down = false;
@ -286,7 +287,7 @@ function addWiLine(ob) {
disableWiSelective(ob.num); disableWiSelective(ob.num);
}); });
$("#constant-key-"+ob.num).on("click", function () { $("#constant-key-"+ob.num).on("click", function () {
element = $("#constant-key-"+ob.num); var element = $("#constant-key-"+ob.num);
if(element.hasClass("constant-key-icon-enabled")) { if(element.hasClass("constant-key-icon-enabled")) {
socket.send({'cmd': 'wiconstantoff', 'data': ob.num}); socket.send({'cmd': 'wiconstantoff', 'data': ob.num});
element.removeClass("constant-key-icon-enabled") element.removeClass("constant-key-icon-enabled")
@ -542,7 +543,7 @@ function hideSaveAsPopup() {
saveaspopup.removeClass("flex"); saveaspopup.removeClass("flex");
saveaspopup.addClass("hidden"); saveaspopup.addClass("hidden");
saveasinput.val(""); saveasinput.val("");
hide([saveasoverwrite]); hide([$(".saveasoverwrite"), $(".popuperror")]);
} }
function sendSaveAsRequest() { function sendSaveAsRequest() {
@ -566,20 +567,67 @@ function buildLoadList(ar) {
showLoadPopup(); showLoadPopup();
var i; var i;
for(i=0; i<ar.length; i++) { for(i=0; i<ar.length; i++) {
loadcontent.append("<div class=\"loadlistitem\" id=\"load"+i+"\" name=\""+ar[i].name+"\">\ loadcontent.append("<div class=\"flex\">\
<div>"+ar[i].name+"</div>\ <div class=\"loadlistpadding\"></div>\
<div>"+ar[i].actions+"</div>\ <span class=\"loadlisticon loadlisticon-delete oi oi-x "+(sman_allow_delete ? "allowed" : "")+"\" id=\"loaddelete"+i+"\" "+(sman_allow_delete ? "title=\"Delete story\"" : "")+" aria-hidden=\"true\"></span>\
<div class=\"loadlistpadding\"></div>\
<span class=\"loadlisticon loadlisticon-rename oi oi-pencil "+(sman_allow_rename ? "allowed" : "")+"\" id=\"loadrename"+i+"\" "+(sman_allow_rename ? "title=\"Rename story\"" : "")+" aria-hidden=\"true\"></span>\
<div class=\"loadlistpadding\"></div>\
<div class=\"loadlistitem\" id=\"load"+i+"\" name=\""+ar[i].name+"\">\
<div>"+ar[i].name+"</div>\
<div class=\"flex-push-right\">"+ar[i].actions+"</div>\
</div>\
</div>"); </div>");
$("#load"+i).on("click", function () { $("#load"+i).on("click", function () {
enableButtons([load_accept]); enableButtons([load_accept]);
socket.send({'cmd': 'loadselect', 'data': $(this).attr("name")}); socket.send({'cmd': 'loadselect', 'data': $(this).attr("name")});
highlightLoadLine($(this)); highlightLoadLine($(this));
}); });
$("#loaddelete"+i).off("click").on("click", (function (name) {
return function () {
if(!sman_allow_delete) {
return;
}
$("#loadcontainerdelete-storyname").text(name);
$("#btn_dsaccept").off("click").on("click", (function (name) {
return function () {
hide([$(".saveasoverwrite"), $(".popuperror")]);
socket.send({'cmd': 'deletestory', 'data': name});
}
})(name));
$("#loadcontainerdelete").removeClass("hidden").addClass("flex");
}
})(ar[i].name));
$("#loadrename"+i).off("click").on("click", (function (name) {
return function () {
if(!sman_allow_rename) {
return;
}
$("#newsavename").val("")
$("#loadcontainerrename-storyname").text(name);
var submit = (function (name) {
return function () {
hide([$(".saveasoverwrite"), $(".popuperror")]);
socket.send({'cmd': 'renamestory', 'data': name, 'newname': $("#newsavename").val()});
}
})(name);
$("#btn_rensaccept").off("click").on("click", submit);
$("#newsavename").off("keydown").on("keydown", function (ev) {
if (ev.which == 13 && $(this).val() != "") {
submit();
}
});
$("#loadcontainerrename").removeClass("hidden").addClass("flex");
$("#newsavename").val(name).select();
}
})(ar[i].name));
} }
} }
function highlightLoadLine(ref) { function highlightLoadLine(ref) {
$("#loadlistcontent > div").removeClass("popuplistselected"); $("#loadlistcontent > div > div.popuplistselected").removeClass("popuplistselected");
ref.addClass("popuplistselected"); ref.addClass("popuplistselected");
} }
@ -687,26 +735,26 @@ function chunkOnKeyDown(event) {
switch(event.keyCode) { switch(event.keyCode) {
case 37: // left case 37: // left
case 39: // right case 39: // right
old_range = getSelection().getRangeAt(0); var old_range = getSelection().getRangeAt(0);
old_range_start = old_range.startOffset; var old_range_start = old_range.startOffset;
old_range_end = old_range.endOffset; var old_range_end = old_range.endOffset;
old_range_ancestor = old_range.commonAncestorContainer; var old_range_ancestor = old_range.commonAncestorContainer;
old_range_start_container = old_range.startContainer; var old_range_start_container = old_range.startContainer;
old_range_end_container = old_range.endContainer; var old_range_end_container = old_range.endContainer;
setTimeout(function () { setTimeout(function () {
// Wait a few milliseconds and check if the caret has moved // Wait a few milliseconds and check if the caret has moved
new_selection = getSelection(); var new_selection = getSelection();
new_range = new_selection.getRangeAt(0); var new_range = new_selection.getRangeAt(0);
if(old_range_start != new_range.startOffset || old_range_end != new_range.endOffset || old_range_ancestor != new_range.commonAncestorContainer || old_range_start_container != new_range.startContainer || old_range_end_container != new_range.endContainer) { if(old_range_start != new_range.startOffset || old_range_end != new_range.endOffset || old_range_ancestor != new_range.commonAncestorContainer || old_range_start_container != new_range.startContainer || old_range_end_container != new_range.endContainer) {
return; return;
} }
// If it hasn't moved, we're at the beginning or end of a chunk // If it hasn't moved, we're at the beginning or end of a chunk
// and the caret must be moved to a different chunk // and the caret must be moved to a different chunk
chunk = document.activeElement; var chunk = document.activeElement;
switch(event.keyCode) { switch(event.keyCode) {
case 37: // left case 37: // left
if((chunk = chunk.previousSibling) && chunk.tagName == "CHUNK") { if((chunk = chunk.previousSibling) && chunk.tagName == "CHUNK") {
range = document.createRange(); var range = document.createRange();
range.selectNodeContents(chunk); range.selectNodeContents(chunk);
range.collapse(false); range.collapse(false);
new_selection.removeAllRanges(); new_selection.removeAllRanges();
@ -723,7 +771,7 @@ function chunkOnKeyDown(event) {
return; return;
case 8: // backspace case 8: // backspace
old_length = document.activeElement.innerText.length; var old_length = document.activeElement.innerText.length;
setTimeout(function () { setTimeout(function () {
// Wait a few milliseconds and compare the chunk's length // Wait a few milliseconds and compare the chunk's length
if(old_length != document.activeElement.innerText.length) { if(old_length != document.activeElement.innerText.length) {
@ -731,8 +779,8 @@ function chunkOnKeyDown(event) {
} }
// If it's the same, we're at the beginning of a chunk // If it's the same, we're at the beginning of a chunk
if((chunk = document.activeElement.previousSibling) && chunk.tagName == "CHUNK") { if((chunk = document.activeElement.previousSibling) && chunk.tagName == "CHUNK") {
range = document.createRange(); var range = document.createRange();
selection = getSelection(); var selection = getSelection();
range.selectNodeContents(chunk); range.selectNodeContents(chunk);
range.collapse(false); range.collapse(false);
selection.removeAllRanges(); selection.removeAllRanges();
@ -771,7 +819,7 @@ function submitEditedChunk(event) {
return; return;
} }
chunk = current_editing_chunk; var chunk = current_editing_chunk;
current_editing_chunk = null; current_editing_chunk = null;
// Submit the edited chunk if it's not empty, otherwise delete it // Submit the edited chunk if it's not empty, otherwise delete it
@ -795,6 +843,8 @@ $(document).ready(function(){
button_save = $('#btn_save'); button_save = $('#btn_save');
button_saveas = $('#btn_saveas'); button_saveas = $('#btn_saveas');
button_savetofile = $('#btn_savetofile'); button_savetofile = $('#btn_savetofile');
button_download = $('#btn_download');
button_downloadtxt= $('#btn_downloadtxt');
button_load = $('#btn_load'); button_load = $('#btn_load');
button_loadfrfile = $('#btn_loadfromfile'); button_loadfrfile = $('#btn_loadfromfile');
button_import = $("#btn_import"); button_import = $("#btn_import");
@ -833,7 +883,6 @@ $(document).ready(function(){
topic = $("#topic"); topic = $("#topic");
saveas_accept = $("#btn_saveasaccept"); saveas_accept = $("#btn_saveasaccept");
saveas_close = $("#btn_saveasclose"); saveas_close = $("#btn_saveasclose");
saveasoverwrite = $("#saveasoverwrite");
loadpopup = $("#loadcontainer"); loadpopup = $("#loadcontainer");
loadcontent = $("#loadlistcontent"); loadcontent = $("#loadlistcontent");
load_accept = $("#btn_loadaccept"); load_accept = $("#btn_loadaccept");
@ -853,6 +902,8 @@ $(document).ready(function(){
socket.on('from_server', function(msg) { socket.on('from_server', function(msg) {
if(msg.cmd == "connected") { if(msg.cmd == "connected") {
// Connected to Server Actions // Connected to Server Actions
sman_allow_delete = msg.hasOwnProperty("smandelete") && msg.smandelete;
sman_allow_rename = msg.hasOwnProperty("smanrename") && msg.smanrename;
connected = true; connected = true;
connect_status.html("<b>Connected to KoboldAI Process!</b>"); connect_status.html("<b>Connected to KoboldAI Process!</b>");
connect_status.removeClass("color_orange"); connect_status.removeClass("color_orange");
@ -870,7 +921,7 @@ $(document).ready(function(){
} }
}); });
} else if(msg.cmd == "updatescreen") { } else if(msg.cmd == "updatescreen") {
_gamestarted = gamestarted; var _gamestarted = gamestarted;
gamestarted = msg.gamestarted; gamestarted = msg.gamestarted;
if(_gamestarted != gamestarted) { if(_gamestarted != gamestarted) {
action_mode = 0; action_mode = 0;
@ -1048,6 +1099,14 @@ $(document).ready(function(){
} else if(msg.cmd == "popupshow") { } else if(msg.cmd == "popupshow") {
// Show/Hide Popup // Show/Hide Popup
popupShow(msg.data); popupShow(msg.data);
} else if(msg.cmd == "hidepopupdelete") {
// Hide the dialog box that asks you to confirm deletion of a story
$("#loadcontainerdelete").removeClass("flex").addClass("hidden");
hide([$(".saveasoverwrite"), $(".popuperror")]);
} else if(msg.cmd == "hidepopuprename") {
// Hide the story renaming dialog box
$("#loadcontainerrename").removeClass("flex").addClass("hidden");
hide([$(".saveasoverwrite"), $(".popuperror")]);
} else if(msg.cmd == "addimportline") { } else if(msg.cmd == "addimportline") {
// Add import popup entry // Add import popup entry
addImportLine(msg.data); addImportLine(msg.data);
@ -1081,7 +1140,11 @@ $(document).ready(function(){
buildLoadList(msg.data); buildLoadList(msg.data);
} else if(msg.cmd == "askforoverwrite") { } else if(msg.cmd == "askforoverwrite") {
// Show overwrite warning // Show overwrite warning
show([saveasoverwrite]); show([$(".saveasoverwrite")]);
} else if(msg.cmd == "popuperror") {
// Show error in the current dialog box
$(".popuperror").text(msg.data);
show([$(".popuperror")]);
} else if(msg.cmd == "genseqs") { } else if(msg.cmd == "genseqs") {
// Parse generator sequences to UI // Parse generator sequences to UI
parsegenseqs(msg.data); parsegenseqs(msg.data);
@ -1211,6 +1274,14 @@ $(document).ready(function(){
saveas_accept.on("click", function(ev) { saveas_accept.on("click", function(ev) {
sendSaveAsRequest(); sendSaveAsRequest();
}); });
button_download.on("click", function(ev) {
window.open("/download", "_blank");
});
button_downloadtxt.on("click", function(ev) {
window.open("/download?format=plaintext", "_blank");
});
button_load.on("click", function(ev) { button_load.on("click", function(ev) {
socket.send({'cmd': 'loadlistrequest', 'data': ''}); socket.send({'cmd': 'loadlistrequest', 'data': ''});
@ -1238,6 +1309,25 @@ $(document).ready(function(){
ns_close.on("click", function(ev) { ns_close.on("click", function(ev) {
hideNewStoryPopup(); hideNewStoryPopup();
}); });
$("#btn_dsclose").on("click", function () {
$("#loadcontainerdelete").removeClass("flex").addClass("hidden");
hide([$(".saveasoverwrite"), $(".popuperror")]);
});
$("#newsavename").on("input", function (ev) {
if($(this).val() == "") {
disableButtons([$("#btn_rensaccept")]);
} else {
enableButtons([$("#btn_rensaccept")]);
}
hide([$(".saveasoverwrite"), $(".popuperror")]);
});
$("#btn_rensclose").on("click", function () {
$("#loadcontainerrename").removeClass("flex").addClass("hidden");
hide([$(".saveasoverwrite"), $(".popuperror")]);
});
button_rndgame.on("click", function(ev) { button_rndgame.on("click", function(ev) {
showRandomStoryPopup(); showRandomStoryPopup();
@ -1262,7 +1352,7 @@ $(document).ready(function(){
} else { } else {
enableButtons([saveas_accept]); enableButtons([saveas_accept]);
} }
hide([saveasoverwrite]); hide([$(".saveasoverwrite"), $(".popuperror")]);
}); });
// Bind Enter button to submit // Bind Enter button to submit

View File

@ -271,12 +271,6 @@ chunk, chunk * {
margin-top: 200px; margin-top: 200px;
} }
#saveasoverwrite {
color: #ff9900;
font-weight: bold;
text-align: center;
}
#loadpopup { #loadpopup {
width: 500px; width: 500px;
background-color: #262626; background-color: #262626;
@ -291,6 +285,18 @@ chunk, chunk * {
} }
} }
#loadpopupdelete {
width: 350px;
background-color: #262626;
margin-top: 200px;
}
#loadpopuprename {
width: 350px;
background-color: #262626;
margin-top: 200px;
}
#loadlistcontent { #loadlistcontent {
height: 325px; height: 325px;
overflow-y: scroll; overflow-y: scroll;
@ -319,6 +325,12 @@ chunk, chunk * {
text-align: center; text-align: center;
} }
.dialogheader {
padding: 10px 40px 10px 40px;
color: #737373;
text-align: center;
}
.anotelabel { .anotelabel {
font-size: 10pt; font-size: 10pt;
color: #ffffff; color: #ffffff;
@ -556,16 +568,16 @@ chunk, chunk * {
} }
.loadlistheader { .loadlistheader {
padding-left: 10px; padding-left: 68px;
display: grid; padding-right: 20px;
grid-template-columns: 80% 20%; display: flex;
color: #737373; color: #737373;
} }
.loadlistitem { .loadlistitem {
padding: 5px 10px 5px 10px; padding: 5px 10px 5px 10px;
display: grid; display: flex;
grid-template-columns: 80% 20%; flex-grow: 1;
color: #ffffff; color: #ffffff;
-moz-transition: background-color 0.25s ease-in; -moz-transition: background-color 0.25s ease-in;
@ -579,6 +591,30 @@ chunk, chunk * {
background-color: #688f1f; background-color: #688f1f;
} }
.loadlistpadding {
padding-right: 10px;
}
.loadlisticon {
color: #333
}
.loadlisticon.allowed {
color: #ddd
}
.loadlisticon.allowed:hover {
cursor: pointer;
}
.loadlisticon-delete.allowed:hover {
color: #ef2929
}
.loadlisticon-rename.allowed:hover {
color: #fce94f
}
.navbar .navbar-nav .nav-link:hover { .navbar .navbar-nav .nav-link:hover {
border-radius: 5px; border-radius: 5px;
background-color: #98bcdb; background-color: #98bcdb;
@ -678,6 +714,11 @@ chunk, chunk * {
font-size: 12pt; font-size: 12pt;
} }
.popuperror {
color: #ef2929;
text-align: center;
}
.popupfooter { .popupfooter {
width: 100%; width: 100%;
padding: 10px; padding: 10px;
@ -692,6 +733,12 @@ chunk, chunk * {
margin-right: 10px; margin-right: 10px;
} }
.saveasoverwrite {
color: #ff9900;
font-weight: bold;
text-align: center;
}
.seqselheader { .seqselheader {
color: #737373; color: #737373;
} }

View File

@ -41,6 +41,8 @@
<a class="dropdown-item" href="#" id="btn_save">Save</a> <a class="dropdown-item" href="#" id="btn_save">Save</a>
<a class="dropdown-item" href="#" id="btn_saveas">Save As</a> <a class="dropdown-item" href="#" id="btn_saveas">Save As</a>
<a class="dropdown-item" href="#" id="btn_savetofile">Save To File...</a> <a class="dropdown-item" href="#" id="btn_savetofile">Save To File...</a>
<a class="dropdown-item" href="#" id="btn_download">Download Story as JSON</a>
<a class="dropdown-item" href="#" id="btn_downloadtxt">Download Story as Plaintext</a>
</div> </div>
</li> </li>
<li class="nav-item dropdown"> <li class="nav-item dropdown">
@ -193,7 +195,10 @@
<div class="aidgpopupcontent"> <div class="aidgpopupcontent">
<input class="form-control" type="text" placeholder="Save Name" id="savename"> <input class="form-control" type="text" placeholder="Save Name" id="savename">
</div> </div>
<div class="hidden" id="saveasoverwrite"> <div class="popuperror hidden">
<span></span>
</div>
<div class="saveasoverwrite hidden">
<span>File already exists. Really overwrite?</span> <span>File already exists. Really overwrite?</span>
</div> </div>
<div class="popupfooter"> <div class="popupfooter">
@ -209,7 +214,7 @@
</div> </div>
<div class="loadlistheader"> <div class="loadlistheader">
<div>Save Name</div> <div>Save Name</div>
<div># Actions</div> <div class="flex-push-right"># Actions</div>
</div> </div>
<div id="loadlistcontent"> <div id="loadlistcontent">
</div> </div>
@ -219,6 +224,44 @@
</div> </div>
</div> </div>
</div> </div>
<div class="popupcontainer hidden" id="loadcontainerdelete">
<div id="loadpopupdelete">
<div class="popuptitlebar">
<div class="popuptitletext">Really Delete Story?</div>
</div>
<div class="dialogheader">
"<span id="loadcontainerdelete-storyname"></span>" will be PERMANENTLY deleted! You will not be able to recover this story later.
</div>
<div class="popuperror hidden">
<span></span>
</div>
<div class="popupfooter">
<button type="button" class="btn btn-danger" id="btn_dsaccept">Delete</button>
<button type="button" class="btn btn-primary" id="btn_dsclose">Cancel</button>
</div>
</div>
</div>
<div class="popupcontainer hidden" id="loadcontainerrename">
<div id="loadpopuprename">
<div class="popuptitlebar">
<div class="popuptitletext">Enter New Name For Story</div>
</div>
<div class="dialogheader">
What should the story "<span id="loadcontainerrename-storyname"></span>" be renamed to?
<input class="form-control" type="text" placeholder="New Save Name" id="newsavename">
</div>
<div class="popuperror hidden">
<span></span>
</div>
<div class="saveasoverwrite hidden">
<span>File already exists. Really overwrite?</span>
</div>
<div class="popupfooter">
<button type="button" class="btn btn-primary" id="btn_rensaccept">Accept</button>
<button type="button" class="btn btn-primary" id="btn_rensclose">Cancel</button>
</div>
</div>
</div>
<div class="popupcontainer hidden" id="newgamecontainer"> <div class="popupcontainer hidden" id="newgamecontainer">
<div id="nspopup"> <div id="nspopup">
<div class="popuptitlebar"> <div class="popuptitlebar">

View File

@ -88,8 +88,8 @@ def addsentencespacing(txt, vars):
# Cleans string for use in file name # Cleans string for use in file name
#==================================================================# #==================================================================#
def cleanfilename(filename): def cleanfilename(filename):
keepcharacters = (' ','.','_') filteredcharacters = ('/','\\')
filename = "".join(c for c in filename if c.isalnum() or c in keepcharacters).rstrip() filename = "".join(c for c in filename if c not in filteredcharacters).rstrip()
return filename return filename