From f9bbb174a6efd8e5ffdb12f2eb4ff007d77efec3 Mon Sep 17 00:00:00 2001 From: KoboldAI Dev Date: Sat, 22 May 2021 05:28:40 -0400 Subject: [PATCH] Added OpenAI API support Added in-browser Save/Load/New Story controls (Force a full refresh in your browser!) Fixed adding InferKit API key if client.settings already exists Added cmd calls to bat files so they'll stay open on error Wait animation now hidden on start state/restart --- aiserver.py | 340 +++++++++++++++++++++++++++++++-------- fileops.py | 26 ++- install_requirements.bat | 2 +- play.bat | 2 +- static/application.js | 249 +++++++++++++++++++++++----- static/custom.css | 60 +++++++ templates/index.html | 73 ++++++++- utils.py | 11 +- 8 files changed, 635 insertions(+), 128 deletions(-) diff --git a/aiserver.py b/aiserver.py index 96cc4ef2..74cd5fd3 100644 --- a/aiserver.py +++ b/aiserver.py @@ -1,6 +1,6 @@ #==================================================================# # KoboldAI Client -# Version: 1.14.0 +# Version: 1.15.0 # By: KoboldAIDev #==================================================================# @@ -43,40 +43,44 @@ modellist = [ ["InferKit API (requires API key)", "InferKit", ""], ["Custom Neo (eg Neo-horni)", "NeoCustom", ""], ["Custom GPT-2 (eg CloverEdition)", "GPT2Custom", ""], - ["Google Colab", "Colab", ""] + ["Google Colab", "Colab", ""], + ["OpenAI API (requires API key)", "OAI", ""] ] # Variables class vars: - lastact = "" # The last action received from the user - lastctx = "" # The last context submitted to the generator - model = "" - noai = False # Runs the script without starting up the transformers pipeline - aibusy = False # Stops submissions while the AI is working - max_length = 512 # Maximum number of tokens to submit per action - ikmax = 3000 # Maximum number of characters to submit to InferKit - genamt = 60 # Amount of text for each action to generate - ikgen = 200 # Number of characters for InferKit to generate - rep_pen = 1.0 # Default generator repetition_penalty - temp = 1.0 # Default generator temperature - top_p = 1.0 # Default generator top_p - gamestarted = False - prompt = "" - memory = "" - authornote = "" + lastact = "" # The last action received from the user + lastctx = "" # The last context submitted to the generator + model = "" # Model ID string chosen at startup + noai = False # Runs the script without starting up the transformers pipeline + aibusy = False # Stops submissions while the AI is working + max_length = 512 # Maximum number of tokens to submit per action + ikmax = 3000 # Maximum number of characters to submit to InferKit + genamt = 60 # Amount of text for each action to generate + ikgen = 200 # Number of characters for InferKit to generate + rep_pen = 1.0 # Default generator repetition_penalty + temp = 1.0 # Default generator temperature + top_p = 1.0 # Default generator top_p + gamestarted = False # Whether the game has started (disables UI elements) + prompt = "" # Prompt + 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 = [] - worldinfo = [] - badwords = [] - badwordsids = [] + actions = [] # Array of 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 deletewi = -1 # Temporary storage for index to delete wirmvwhtsp = False # Whether to remove leading whitespace from WI entries widepth = 1 # How many historical actions to scan for WI hits mode = "play" # Whether the interface is in play, memory, or edit mode editln = 0 # Which line was last selected in Edit Mode url = "https://api.inferkit.com/v1/models/standard/generate" # InferKit API URL + oaiurl = "" # OpenAI API URL + oaiengines = "https://api.openai.com/v1/engines" colaburl = "" # Ngrok url for Google Colab mode apikey = "" # API key to use for InferKit API calls + oaiapikey = "" # API key to use for OpenAI API calls savedir = getcwd()+"\stories" hascuda = False # Whether torch has detected CUDA on the system usegpu = False # Whether to launch pipeline with GPU support @@ -84,6 +88,9 @@ class vars: formatoptns = {} # Container for state of formatting options importnum = -1 # Selection on import popup list importjs = {} # Temporary storage for import data + loadselect = "" # Temporary storage for filename to load + svowname = "" + saveow = False #==================================================================# # Function to get model selection at startup @@ -138,7 +145,7 @@ print("{0}Welcome to the KoboldAI Client!\nSelect an AI model to continue:{1}\n" getModelSelection() # If transformers model was selected & GPU available, ask to use CPU or GPU -if(not vars.model in ["InferKit", "Colab"]): +if(not vars.model in ["InferKit", "Colab", "OAI"]): # Test for GPU support import torch print("{0}Looking for GPU support...{1}".format(colors.PURPLE, colors.END), end="") @@ -185,7 +192,7 @@ if(vars.model == "InferKit"): file = open("client.settings", "r") # Check if API key exists js = json.load(file) - if(js["apikey"] != ""): + if("apikey" in js and js["apikey"] != ""): # API key exists, grab it and close the file vars.apikey = js["apikey"] file.close() @@ -201,6 +208,73 @@ if(vars.model == "InferKit"): finally: file.close() +# Ask for API key if OpenAI was selected +if(vars.model == "OAI"): + if(not path.exists("client.settings")): + # If the client settings file doesn't exist, create it + print("{0}Please enter your OpenAI API key:{1}\n".format(colors.CYAN, colors.END)) + vars.oaiapikey = input("Key> ") + # Write API key to file + file = open("client.settings", "w") + try: + js = {"oaiapikey": vars.oaiapikey} + file.write(json.dumps(js, indent=3)) + finally: + file.close() + else: + # Otherwise open it up + file = open("client.settings", "r") + # Check if API key exists + js = json.load(file) + if("oaiapikey" in js and js["oaiapikey"] != ""): + # API key exists, grab it and close the file + vars.oaiapikey = js["oaiapikey"] + file.close() + else: + # Get API key, add it to settings object, and write it to disk + print("{0}Please enter your OpenAI API key:{1}\n".format(colors.CYAN, colors.END)) + vars.oaiapikey = input("Key> ") + js["oaiapikey"] = vars.oaiapikey + # Write API key to file + file = open("client.settings", "w") + try: + file.write(json.dumps(js, indent=3)) + finally: + file.close() + + # Get list of models from OAI + print("{0}Retrieving engine list...{1}".format(colors.PURPLE, colors.END), end="") + req = requests.get( + vars.oaiengines, + headers = { + 'Authorization': 'Bearer '+vars.oaiapikey + } + ) + if(req.status_code == 200): + print("{0}OK!{1}".format(colors.GREEN, colors.END)) + print("{0}Please select an engine to use:{1}\n".format(colors.CYAN, colors.END)) + engines = req.json()["data"] + # Print list of engines + i = 0 + for en in engines: + print(" {0} - {1} ({2})".format(i, en["id"], "\033[92mready\033[0m" if en["ready"] == True else "\033[91mnot ready\033[0m")) + i += 1 + # Get engine to use + print("") + engselected = False + while(engselected == False): + engine = input("Engine #> ") + if(engine.isnumeric() and int(engine) < len(engines)): + vars.oaiurl = "https://api.openai.com/v1/engines/{0}/completions".format(engines[int(engine)]["id"]) + engselected = True + else: + print("{0}Please enter a valid selection.{1}".format(colors.RED, colors.END)) + else: + # Something went wrong, print the message and quit since we can't initialize an engine + print("{0}ERROR!{1}".format(colors.RED, colors.END)) + print(req.json()) + quit() + # Ask for ngrok url if Google Colab was selected if(vars.model == "Colab"): print("{0}Please enter the ngrok.io URL displayed in Google Colab:{1}\n".format(colors.CYAN, colors.END)) @@ -221,7 +295,7 @@ socketio = SocketIO(app) print("{0}OK!{1}".format(colors.GREEN, colors.END)) # Start transformers and create pipeline -if(not vars.model in ["InferKit", "Colab"]): +if(not vars.model in ["InferKit", "Colab", "OAI"]): if(not vars.noai): print("{0}Initializing transformers, please wait...{1}".format(colors.PURPLE, colors.END)) from transformers import pipeline, GPT2Tokenizer, GPT2LMHeadModel, GPTNeoForCausalLM @@ -262,10 +336,13 @@ if(not vars.model in ["InferKit", "Colab"]): print("{0}OK! {1} pipeline created!{2}".format(colors.GREEN, vars.model, colors.END)) else: - # If we're running Colab, we still need a tokenizer. + # If we're running Colab or OAI, we still need a tokenizer. if(vars.model == "Colab"): from transformers import GPT2Tokenizer tokenizer = GPT2Tokenizer.from_pretrained("EleutherAI/gpt-neo-2.7B") + elif(vars.model == "OAI"): + from transformers import GPT2Tokenizer + tokenizer = GPT2Tokenizer.from_pretrained("gpt2") # Set up Flask routes @app.route('/') @@ -354,10 +431,10 @@ def get_message(msg): deleterequest() elif(msg['cmd'] == 'memory'): togglememorymode() - elif(msg['cmd'] == 'save'): - saveRequest() - elif(msg['cmd'] == 'load'): - loadRequest() + elif(msg['cmd'] == 'savetofile'): + savetofile() + elif(msg['cmd'] == 'loadfromfile'): + loadfromfile() elif(msg['cmd'] == 'import'): importRequest() elif(msg['cmd'] == 'newgame'): @@ -431,16 +508,29 @@ def get_message(msg): commitwi(msg['data']) elif(msg['cmd'] == 'aidgimport'): importAidgRequest(msg['data']) + elif(msg['cmd'] == 'saveasrequest'): + saveas(msg['data']) + elif(msg['cmd'] == 'saverequest'): + save() + elif(msg['cmd'] == 'loadlistrequest'): + getloadlist() + elif(msg['cmd'] == 'loadselect'): + vars.loadselect = msg["data"] + elif(msg['cmd'] == 'loadrequest'): + loadRequest(getcwd()+"/stories/"+vars.loadselect+".json") + elif(msg['cmd'] == 'clearoverwrite'): + vars.svowname = "" + vars.saveow = False #==================================================================# -# +# Send start message and tell Javascript to set UI state #==================================================================# def setStartState(): emit('from_server', {'cmd': 'updatescreen', 'data': 'Welcome to KoboldAI Client! You are running '+vars.model+'.
Please load a game or enter a prompt below to begin!
'}) emit('from_server', {'cmd': 'setgamestate', 'data': 'start'}) #==================================================================# -# +# Transmit applicable settings to SocketIO to build UI sliders/toggles #==================================================================# def sendsettings(): # Send settings for selected AI type @@ -459,7 +549,7 @@ def sendsettings(): vars.formatoptns[frm["id"]] = False; #==================================================================# -# +# Take settings from vars and write them to client settings file #==================================================================# def savesettings(): # Build json to write @@ -482,7 +572,7 @@ def savesettings(): file.close() #==================================================================# -# +# Read settings from client file JSON and send to vars #==================================================================# def loadsettings(): if(path.exists("client.settings")): @@ -513,7 +603,7 @@ def loadsettings(): file.close() #==================================================================# -# +# Don't save settings unless 2 seconds have passed without modification #==================================================================# @debounce(2) def settingschanged(): @@ -521,7 +611,7 @@ def settingschanged(): savesettings() #==================================================================# -# +# Take input text from SocketIO and decide what to do with it #==================================================================# def actionsubmit(data): # Ignore new submissions if the AI is currently busy @@ -601,10 +691,12 @@ def calcsubmit(txt): subtxt = vars.memory + winfo + anotetxt + vars.prompt lnsub = lnmem + lnwi + lnprompt + lnanote - if(vars.model != "Colab"): + if(not vars.model in ["Colab", "OAI"]): generate(subtxt, lnsub+1, lnsub+vars.genamt) - else: + elif(vars.model == "Colab"): sendtocolab(subtxt, lnsub+1, lnsub+vars.genamt) + elif(vars.model == "OAI"): + oairequest(subtxt, lnsub+1, lnsub+vars.genamt) else: tokens = [] @@ -643,23 +735,28 @@ def calcsubmit(txt): # Prepend Memory, WI, and Prompt before action tokens tokens = memtokens + witokens + prompttkns + tokens - - # Send completed bundle to generator ln = len(tokens) - if(vars.model != "Colab"): + if(not vars.model in ["Colab", "OAI"]): generate ( tokenizer.decode(tokens), ln+1, ln+vars.genamt ) - else: + elif(vars.model == "Colab"): sendtocolab( tokenizer.decode(tokens), ln+1, ln+vars.genamt ) + elif(vars.model == "OAI"): + oairequest( + tokenizer.decode(tokens), + ln+1, + ln+vars.genamt + ) + # For InferKit web API else: @@ -1151,6 +1248,55 @@ def ikrequest(txt): emit('from_server', {'cmd': 'errmsg', 'data': errmsg}) set_aibusy(0) +#==================================================================# +# Assembles game data into a request to OpenAI API +#==================================================================# +def oairequest(txt, min, max): + # Log request to console + print("{0}Len:{1}, Txt:{2}{3}".format(colors.YELLOW, len(txt), txt, colors.END)) + + # Store context in memory to use it for comparison with generated content + vars.lastctx = txt + + # Build request JSON data + reqdata = { + 'prompt': txt, + 'max_tokens': max, + 'temperature': vars.temp, + 'top_p': vars.top_p, + 'n': 1, + 'stream': False + } + + req = requests.post( + vars.oaiurl, + json = reqdata, + headers = { + 'Authorization': 'Bearer '+vars.oaiapikey, + 'Content-Type': 'application/json' + } + ) + + # Deal with the response + if(req.status_code == 200): + 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)}) + + set_aibusy(0) + else: + # Send error message to web client + er = req.json() + if("error" in er): + type = er["error"]["type"] + message = er["error"]["message"] + + errmsg = "OpenAI API Error: {0} - {1}".format(type, message) + emit('from_server', {'cmd': 'errmsg', 'data': errmsg}) + set_aibusy(0) + #==================================================================# # Forces UI to Play mode #==================================================================# @@ -1164,11 +1310,44 @@ def exitModes(): vars.mode = "play" #==================================================================# -# Save the story to a file +# Launch in-browser save prompt #==================================================================# -def saveRequest(): +def saveas(name): + # Check if filename exists already + name = utils.cleanfilename(name) + if(not fileops.saveexists(name) or (vars.saveow and vars.svowname == name)): + # All clear to save + saveRequest(getcwd()+"/stories/"+name+".json") + emit('from_server', {'cmd': 'hidesaveas', 'data': ''}) + vars.saveow = False + vars.svowname = "" + else: + # File exists, prompt for overwrite + vars.saveow = True + vars.svowname = name + emit('from_server', {'cmd': 'askforoverwrite', 'data': ''}) + +#==================================================================# +# Save the currently running story +#==================================================================# +def save(): + # Check if a file is currently open + if(".json" in vars.savedir): + saveRequest(vars.savedir) + else: + emit('from_server', {'cmd': 'saveas', 'data': ''}) + +#==================================================================# +# Save the story via file browser +#==================================================================# +def savetofile(): savpath = fileops.getsavepath(vars.savedir, "Save Story As", [("Json", "*.json")]) - + saveRequest(savpath) + +#==================================================================# +# Save the story to specified path +#==================================================================# +def saveRequest(savpath): if(savpath): # Leave Edit/Memory mode before continuing exitModes() @@ -1200,12 +1379,23 @@ def saveRequest(): finally: file.close() +#==================================================================# +# Load a saved story via file browser +#==================================================================# +def getloadlist(): + emit('from_server', {'cmd': 'buildload', 'data': fileops.getstoryfiles()}) + +#==================================================================# +# Load a saved story via file browser +#==================================================================# +def loadfromfile(): + loadpath = fileops.getloadpath(vars.savedir, "Select Story File", [("Json", "*.json")]) + loadRequest(loadpath) + #==================================================================# # Load a stored story from a file #==================================================================# -def loadRequest(): - loadpath = fileops.getloadpath(vars.savedir, "Select Story File", [("Json", "*.json")]) - +def loadRequest(loadpath): if(loadpath): # Leave Edit/Memory mode before continuing exitModes() @@ -1242,6 +1432,12 @@ def loadRequest(): file.close() + # Save path for save button + vars.savedir = loadpath + + # Clear loadselect var + vars.loadselect = "" + # Refresh game screen sendwi() refresh_story() @@ -1351,6 +1547,9 @@ def importgame(): # Clear import data vars.importjs = {} + # Reset current save + vars.savedir = getcwd()+"\stories" + # Refresh game screen sendwi() refresh_story() @@ -1388,6 +1587,9 @@ def importAidgRequest(id): }) num += 1 + # Reset current save + vars.savedir = getcwd()+"\stories" + # Refresh game screen sendwi() refresh_story() @@ -1397,30 +1599,26 @@ def importAidgRequest(id): # Starts a new story #==================================================================# def newGameRequest(): - # Ask for confirmation - root = tk.Tk() - root.attributes("-topmost", True) - confirm = tk.messagebox.askquestion("Confirm New Game", "Really start new Story?") - root.destroy() + # Leave Edit/Memory mode before continuing + exitModes() - if(confirm == "yes"): - # Leave Edit/Memory mode before continuing - exitModes() - - # Clear vars values - vars.gamestarted = False - vars.prompt = "" - vars.memory = "" - vars.actions = [] - vars.savedir = getcwd()+"\stories" - vars.authornote = "" - vars.worldinfo = [] - vars.lastact = "" - vars.lastctx = "" - - # Refresh game screen - sendwi() - setStartState() + # Clear vars values + vars.gamestarted = False + vars.prompt = "" + vars.memory = "" + vars.actions = [] + + vars.authornote = "" + vars.worldinfo = [] + vars.lastact = "" + vars.lastctx = "" + + # Reset current save + vars.savedir = getcwd()+"\stories" + + # Refresh game screen + sendwi() + setStartState() #==================================================================# diff --git a/fileops.py b/fileops.py index e17d85c0..dc5cb66e 100644 --- a/fileops.py +++ b/fileops.py @@ -1,5 +1,7 @@ import tkinter as tk from tkinter import filedialog +from os import getcwd, listdir, path +import json #==================================================================# # Generic Method for prompting for file path @@ -50,4 +52,26 @@ def getdirpath(dir, title): if(path != "" and path != None): return path else: - return None \ No newline at end of file + return None + +#==================================================================# +# Returns an array of dicts containing story files in /stories +#==================================================================# +def getstoryfiles(): + list = [] + for file in listdir(getcwd()+"/stories"): + if file.endswith(".json"): + ob = {} + ob["name"] = file.replace(".json", "") + f = open(getcwd()+"/stories/"+file, "r") + js = json.load(f) + f.close() + ob["actions"] = len(js["actions"]) + list.append(ob) + return list + +#==================================================================# +# Returns True if json file exists with requested save name +#==================================================================# +def saveexists(name): + return path.exists(getcwd()+"/stories/"+name+".json") \ No newline at end of file diff --git a/install_requirements.bat b/install_requirements.bat index 39a9971b..4f5e5b11 100644 --- a/install_requirements.bat +++ b/install_requirements.bat @@ -1 +1 @@ -pip install -r requirements.txt \ No newline at end of file +start cmd /k pip install -r requirements.txt \ No newline at end of file diff --git a/play.bat b/play.bat index 75f00927..2d9db428 100644 --- a/play.bat +++ b/play.bat @@ -1 +1 @@ -py -3 aiserver.py \ No newline at end of file +start cmd /k py -3 aiserver.py \ No newline at end of file diff --git a/static/application.js b/static/application.js index 8ac1ef61..7adbbc62 100644 --- a/static/application.js +++ b/static/application.js @@ -9,6 +9,8 @@ var socket; var connect_status; var button_newgame; var button_save; +var button_saveas; +var button_savetofile; var button_load; var button_import; var button_impaidg; @@ -40,6 +42,18 @@ var aidgpopup; var aidgpromptnum; var aidg_accept; var aidg_close; +var saveaspopup; +var saveasinput; +var saveas_accept; +var saveas_close; +var saveasoverwrite; +var loadpopup; +var loadcontent; +var load_accept; +var load_close; +var nspopup; +var ns_accept; +var ns_close; // Key states var shift_down = false; @@ -201,6 +215,7 @@ function hideWiDeleteConfirm(num) { function highlightImportLine(ref) { $("#popupcontent > div").removeClass("popuplistselected"); ref.addClass("popuplistselected"); + enableButtons([popup_accept]); } function enableButtons(refs) { @@ -270,6 +285,7 @@ function popupShow(state) { if(state) { popup.removeClass("hidden"); popup.addClass("flex"); + disableButtons([popup_accept]); } else { popup.removeClass("flex"); popup.addClass("hidden"); @@ -385,6 +401,68 @@ function sendAidgImportRequest() { aidgpromptnum.val(""); } +function showSaveAsPopup() { + disableButtons([saveas_accept]); + saveaspopup.removeClass("hidden"); + saveaspopup.addClass("flex"); + saveasinput.focus(); +} + +function hideSaveAsPopup() { + saveaspopup.removeClass("flex"); + saveaspopup.addClass("hidden"); + saveasinput.val(""); + hide([saveasoverwrite]); +} + +function sendSaveAsRequest() { + socket.send({'cmd': 'saveasrequest', 'data': saveasinput.val()}); +} + +function showLoadPopup() { + loadpopup.removeClass("hidden"); + loadpopup.addClass("flex"); +} + +function hideLoadPopup() { + loadpopup.removeClass("flex"); + loadpopup.addClass("hidden"); + loadcontent.html(""); +} + +function buildLoadList(ar) { + disableButtons([load_accept]); + loadcontent.html(""); + showLoadPopup(); + var i; + for(i=0; i\ +
"+ar[i].name+"
\ +
"+ar[i].actions+"
\ + "); + $("#load"+i).on("click", function () { + enableButtons([load_accept]); + socket.send({'cmd': 'loadselect', 'data': $(this).attr("name")}); + highlightLoadLine($(this)); + }); + } +} + +function highlightLoadLine(ref) { + $("#loadlistcontent > div").removeClass("popuplistselected"); + ref.addClass("popuplistselected"); +} + +function showNewStoryPopup() { + nspopup.removeClass("hidden"); + nspopup.addClass("flex"); +} + +function hideNewStoryPopup() { + nspopup.removeClass("flex"); + nspopup.addClass("hidden"); +} + //=================================================================// // READY/RUNTIME //=================================================================// @@ -392,40 +470,55 @@ function sendAidgImportRequest() { $(document).ready(function(){ // Bind UI references - connect_status = $('#connectstatus'); - button_newgame = $('#btn_newgame'); - button_save = $('#btn_save'); - button_load = $('#btn_load'); - button_import = $("#btn_import"); - button_impaidg = $("#btn_impaidg"); - button_settings = $('#btn_settings'); - button_format = $('#btn_format'); - button_send = $('#btnsend'); - button_actedit = $('#btn_actedit'); - button_actmem = $('#btn_actmem'); - button_actback = $('#btn_actundo'); - button_actretry = $('#btn_actretry'); - button_delete = $('#btn_delete'); - button_actwi = $('#btn_actwi'); - game_text = $('#gametext'); - input_text = $('#input_text'); - message_text = $('#messagefield'); - settings_menu = $("#settingsmenu"); - format_menu = $('#formatmenu'); - anote_menu = $('#anoterowcontainer'); - wi_menu = $('#wimenu'); - anote_input = $('#anoteinput'); - anote_labelcur = $('#anotecur'); - anote_slider = $('#anotedepth'); - popup = $("#popupcontainer"); - popup_title = $("#popuptitletext"); - popup_content = $("#popupcontent"); - popup_accept = $("#btn_popupaccept"); - popup_close = $("#btn_popupclose"); - aidgpopup = $("#aidgpopupcontainer"); - aidgpromptnum = $("#aidgpromptnum"); - aidg_accept = $("#btn_aidgpopupaccept"); - aidg_close = $("#btn_aidgpopupclose"); + connect_status = $('#connectstatus'); + button_newgame = $('#btn_newgame'); + button_save = $('#btn_save'); + button_saveas = $('#btn_saveas'); + button_savetofile = $('#btn_savetofile'); + button_load = $('#btn_load'); + button_loadfrfile = $('#btn_loadfromfile'); + button_import = $("#btn_import"); + button_impaidg = $("#btn_impaidg"); + button_settings = $('#btn_settings'); + button_format = $('#btn_format'); + button_send = $('#btnsend'); + button_actedit = $('#btn_actedit'); + button_actmem = $('#btn_actmem'); + button_actback = $('#btn_actundo'); + button_actretry = $('#btn_actretry'); + button_delete = $('#btn_delete'); + button_actwi = $('#btn_actwi'); + game_text = $('#gametext'); + input_text = $('#input_text'); + message_text = $('#messagefield'); + settings_menu = $("#settingsmenu"); + format_menu = $('#formatmenu'); + anote_menu = $('#anoterowcontainer'); + wi_menu = $('#wimenu'); + anote_input = $('#anoteinput'); + anote_labelcur = $('#anotecur'); + anote_slider = $('#anotedepth'); + popup = $("#popupcontainer"); + popup_title = $("#popuptitletext"); + popup_content = $("#popupcontent"); + popup_accept = $("#btn_popupaccept"); + popup_close = $("#btn_popupclose"); + aidgpopup = $("#aidgpopupcontainer"); + aidgpromptnum = $("#aidgpromptnum"); + aidg_accept = $("#btn_aidgpopupaccept"); + aidg_close = $("#btn_aidgpopupclose"); + saveaspopup = $("#saveascontainer"); + saveasinput = $("#savename"); + saveas_accept = $("#btn_saveasaccept"); + saveas_close = $("#btn_saveasclose"); + saveasoverwrite = $("#saveasoverwrite"); + loadpopup = $("#loadcontainer"); + loadcontent = $("#loadlistcontent"); + load_accept = $("#btn_loadaccept"); + load_close = $("#btn_loadclose"); + nspopup = $("#newgamecontainer"); + ns_accept = $("#btn_nsaccept"); + ns_close = $("#btn_nsclose"); // Connect to SocketIO server loc = window.document.location; @@ -465,10 +558,14 @@ $(document).ready(function(){ hide([wi_menu, button_delete]); show([game_text, button_actedit, button_actmem, button_actwi, button_actback, button_actretry]); hideMessage(); + hideWaitAnimation(); button_actedit.html("Edit"); button_actmem.html("Memory"); button_actwi.html("W Info"); hideAidgPopup(); + hideSaveAsPopup(); + hideLoadPopup(); + hideNewStoryPopup(); } } else if(msg.cmd == "editmode") { // Enable or Disable edit mode @@ -598,8 +695,20 @@ $(document).ready(function(){ } else if(msg.cmd == "requestwiitem") { // Package WI contents and send back to server returnWiList(msg.data); + } else if(msg.cmd == "saveas") { + // Show Save As prompt + showSaveAsPopup(); + } else if(msg.cmd == "hidesaveas") { + // Hide Save As prompt + hideSaveAsPopup(); + } else if(msg.cmd == "buildload") { + // Send array of save files to load UI + buildLoadList(msg.data); + } else if(msg.cmd == "askforoverwrite") { + // Show overwrite warning + show([saveasoverwrite]); } - }); + }); socket.on('disconnect', function() { connect_status.html("Lost connection..."); @@ -632,22 +741,18 @@ $(document).ready(function(){ socket.send({'cmd': 'memory', 'data': ''}); }); - button_save.on("click", function(ev) { - socket.send({'cmd': 'save', 'data': ''}); + button_savetofile.on("click", function(ev) { + socket.send({'cmd': 'savetofile', 'data': ''}); }); - button_load.on("click", function(ev) { - socket.send({'cmd': 'load', 'data': ''}); + button_loadfrfile.on("click", function(ev) { + socket.send({'cmd': 'loadfromfile', 'data': ''}); }); button_import.on("click", function(ev) { socket.send({'cmd': 'import', 'data': ''}); }); - button_newgame.on("click", function(ev) { - socket.send({'cmd': 'newgame', 'data': ''}); - }); - button_settings.on("click", function(ev) { $('#settingsmenu').slideToggle("slow"); }); @@ -680,6 +785,58 @@ $(document).ready(function(){ sendAidgImportRequest(); }); + button_save.on("click", function(ev) { + socket.send({'cmd': 'saverequest', 'data': ''}); + }); + + button_saveas.on("click", function(ev) { + showSaveAsPopup(); + }); + + saveas_close.on("click", function(ev) { + hideSaveAsPopup(); + socket.send({'cmd': 'clearoverwrite', 'data': ''}); + }); + + saveas_accept.on("click", function(ev) { + sendSaveAsRequest(); + }); + + button_load.on("click", function(ev) { + socket.send({'cmd': 'loadlistrequest', 'data': ''}); + }); + + load_close.on("click", function(ev) { + hideLoadPopup(); + }); + + load_accept.on("click", function(ev) { + socket.send({'cmd': 'loadrequest', 'data': ''}); + hideLoadPopup(); + }); + + button_newgame.on("click", function(ev) { + showNewStoryPopup(); + }); + + ns_accept.on("click", function(ev) { + socket.send({'cmd': 'newgame', 'data': ''}); + hideNewStoryPopup(); + }); + + ns_close.on("click", function(ev) { + hideNewStoryPopup(); + }); + + saveasinput.on("input", function () { + if(saveasinput.val() == "") { + disableButtons([saveas_accept]); + } else { + enableButtons([saveas_accept]); + } + hide([saveasoverwrite]); + }); + // Bind Enter button to submit input_text.keydown(function (ev) { if (ev.which == 13 && !shift_down) { @@ -705,5 +862,11 @@ $(document).ready(function(){ sendAidgImportRequest(); } }); + + saveasinput.keydown(function (ev) { + if (ev.which == 13 && saveasinput.val() != "") { + sendSaveAsRequest(); + } + }); }); diff --git a/static/custom.css b/static/custom.css index 8ee0791b..56db8eae 100644 --- a/static/custom.css +++ b/static/custom.css @@ -226,6 +226,43 @@ chunk { color: #ffffff; } +#saveaspopup { + width: 350px; + background-color: #262626; + margin-top: 200px; +} + +#saveasoverwrite { + color: #ff9900; + font-weight: bold; + text-align: center; +} + +#loadpopup { + width: 500px; + background-color: #262626; + margin-top: 100px; +} + +@media (max-width: 768px) { + #loadpopup { + width: 100%; + background-color: #262626; + margin-top: 100px; + } +} + +#loadlistcontent { + height: 325px; + overflow-y: scroll; +} + +#nspopup { + width: 350px; + background-color: #262626; + margin-top: 200px; +} + /*================= Classes =================*/ .aidgpopupcontent { @@ -381,6 +418,29 @@ chunk { text-align: right; } +.loadlistheader { + padding-left: 10px; + display: grid; + grid-template-columns: 80% 20%; + color: #737373; +} + +.loadlistitem { + padding: 5px 10px 5px 10px; + display: grid; + grid-template-columns: 80% 20%; + 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; +} + +.loadlistitem:hover { + cursor: pointer; + background-color: #688f1f; +} + .navbar .navbar-nav .nav-link:hover { border-radius: 5px; background-color: #98bcdb; diff --git a/templates/index.html b/templates/index.html index 6ad40e70..569dd6ed 100644 --- a/templates/index.html +++ b/templates/index.html @@ -6,13 +6,13 @@ - + - +
@@ -30,14 +30,23 @@ - - +
+ + + \ No newline at end of file diff --git a/utils.py b/utils.py index 816b1af9..fd46b8db 100644 --- a/utils.py +++ b/utils.py @@ -80,9 +80,14 @@ def addsentencespacing(txt, vars): if(lastchar == "." or lastchar == "!" or lastchar == "?" or lastchar == "," or lastchar == ";" or lastchar == ":"): txt = " " + txt return txt - - - + +#==================================================================# +# Cleans string for use in file name +#==================================================================# +def cleanfilename(filename): + keepcharacters = (' ','.','_') + filename = "".join(c for c in filename if c.isalnum() or c in keepcharacters).rstrip() + return filename