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
This commit is contained in:
KoboldAI Dev 2021-05-22 05:28:40 -04:00
parent 4996e0ff46
commit f9bbb174a6
8 changed files with 635 additions and 128 deletions

View File

@ -1,6 +1,6 @@
#==================================================================# #==================================================================#
# KoboldAI Client # KoboldAI Client
# Version: 1.14.0 # Version: 1.15.0
# By: KoboldAIDev # By: KoboldAIDev
#==================================================================# #==================================================================#
@ -43,40 +43,44 @@ modellist = [
["InferKit API (requires API key)", "InferKit", ""], ["InferKit API (requires API key)", "InferKit", ""],
["Custom Neo (eg Neo-horni)", "NeoCustom", ""], ["Custom Neo (eg Neo-horni)", "NeoCustom", ""],
["Custom GPT-2 (eg CloverEdition)", "GPT2Custom", ""], ["Custom GPT-2 (eg CloverEdition)", "GPT2Custom", ""],
["Google Colab", "Colab", ""] ["Google Colab", "Colab", ""],
["OpenAI API (requires API key)", "OAI", ""]
] ]
# Variables # Variables
class vars: class vars:
lastact = "" # The last action received from the user lastact = "" # The last action received from the user
lastctx = "" # The last context submitted to the generator lastctx = "" # The last context submitted to the generator
model = "" model = "" # Model ID string chosen at startup
noai = False # Runs the script without starting up the transformers pipeline noai = False # Runs the script without starting up the transformers pipeline
aibusy = False # Stops submissions while the AI is working aibusy = False # Stops submissions while the AI is working
max_length = 512 # Maximum number of tokens to submit per action max_length = 512 # Maximum number of tokens to submit per action
ikmax = 3000 # Maximum number of characters to submit to InferKit ikmax = 3000 # Maximum number of characters to submit to InferKit
genamt = 60 # Amount of text for each action to generate genamt = 60 # Amount of text for each action to generate
ikgen = 200 # Number of characters for InferKit to generate ikgen = 200 # Number of characters for InferKit to generate
rep_pen = 1.0 # Default generator repetition_penalty rep_pen = 1.0 # Default generator repetition_penalty
temp = 1.0 # Default generator temperature temp = 1.0 # Default generator temperature
top_p = 1.0 # Default generator top_p top_p = 1.0 # Default generator top_p
gamestarted = False gamestarted = False # Whether the game has started (disables UI elements)
prompt = "" prompt = "" # Prompt
memory = "" memory = "" # Text submitted to memory field
authornote = "" authornote = "" # Text submitted to Author's Note field
andepth = 3 # How far back in history to append author's note andepth = 3 # How far back in history to append author's note
actions = [] actions = [] # Array of actions submitted by user and AI
worldinfo = [] worldinfo = [] # Array of World Info key/value objects
badwords = [] badwords = [] # Array of str/chr values that should be removed from output
badwordsids = [] badwordsids = [] # Tokenized array of badwords
deletewi = -1 # Temporary storage for index to delete deletewi = -1 # Temporary storage for index to delete
wirmvwhtsp = False # Whether to remove leading whitespace from WI entries wirmvwhtsp = False # Whether to remove leading whitespace from WI entries
widepth = 1 # How many historical actions to scan for WI hits widepth = 1 # How many historical actions to scan for WI hits
mode = "play" # Whether the interface is in play, memory, or edit mode mode = "play" # Whether the interface is in play, memory, or edit mode
editln = 0 # Which line was last selected in Edit Mode editln = 0 # Which line was last selected in Edit Mode
url = "https://api.inferkit.com/v1/models/standard/generate" # InferKit API URL 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 colaburl = "" # Ngrok url for Google Colab mode
apikey = "" # API key to use for InferKit API calls apikey = "" # API key to use for InferKit API calls
oaiapikey = "" # API key to use for OpenAI API calls
savedir = getcwd()+"\stories" savedir = getcwd()+"\stories"
hascuda = False # Whether torch has detected CUDA on the system hascuda = False # Whether torch has detected CUDA on the system
usegpu = False # Whether to launch pipeline with GPU support usegpu = False # Whether to launch pipeline with GPU support
@ -84,6 +88,9 @@ class vars:
formatoptns = {} # Container for state of formatting options formatoptns = {} # Container for state of formatting options
importnum = -1 # Selection on import popup list importnum = -1 # Selection on import popup list
importjs = {} # Temporary storage for import data importjs = {} # Temporary storage for import data
loadselect = "" # Temporary storage for filename to load
svowname = ""
saveow = False
#==================================================================# #==================================================================#
# Function to get model selection at startup # 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() getModelSelection()
# If transformers model was selected & GPU available, ask to use CPU or GPU # 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 # Test for GPU support
import torch import torch
print("{0}Looking for GPU support...{1}".format(colors.PURPLE, colors.END), end="") 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") file = open("client.settings", "r")
# Check if API key exists # Check if API key exists
js = json.load(file) js = json.load(file)
if(js["apikey"] != ""): if("apikey" in js and js["apikey"] != ""):
# API key exists, grab it and close the file # API key exists, grab it and close the file
vars.apikey = js["apikey"] vars.apikey = js["apikey"]
file.close() file.close()
@ -201,6 +208,73 @@ if(vars.model == "InferKit"):
finally: finally:
file.close() 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 # Ask for ngrok url if Google Colab was selected
if(vars.model == "Colab"): if(vars.model == "Colab"):
print("{0}Please enter the ngrok.io URL displayed in Google Colab:{1}\n".format(colors.CYAN, colors.END)) 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)) print("{0}OK!{1}".format(colors.GREEN, colors.END))
# Start transformers and create pipeline # Start transformers and create pipeline
if(not vars.model in ["InferKit", "Colab"]): if(not vars.model in ["InferKit", "Colab", "OAI"]):
if(not vars.noai): if(not vars.noai):
print("{0}Initializing transformers, please wait...{1}".format(colors.PURPLE, colors.END)) print("{0}Initializing transformers, please wait...{1}".format(colors.PURPLE, colors.END))
from transformers import pipeline, GPT2Tokenizer, GPT2LMHeadModel, GPTNeoForCausalLM 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)) print("{0}OK! {1} pipeline created!{2}".format(colors.GREEN, vars.model, colors.END))
else: 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"): if(vars.model == "Colab"):
from transformers import GPT2Tokenizer from transformers import GPT2Tokenizer
tokenizer = GPT2Tokenizer.from_pretrained("EleutherAI/gpt-neo-2.7B") 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 # Set up Flask routes
@app.route('/') @app.route('/')
@ -354,10 +431,10 @@ def get_message(msg):
deleterequest() deleterequest()
elif(msg['cmd'] == 'memory'): elif(msg['cmd'] == 'memory'):
togglememorymode() togglememorymode()
elif(msg['cmd'] == 'save'): elif(msg['cmd'] == 'savetofile'):
saveRequest() savetofile()
elif(msg['cmd'] == 'load'): elif(msg['cmd'] == 'loadfromfile'):
loadRequest() loadfromfile()
elif(msg['cmd'] == 'import'): elif(msg['cmd'] == 'import'):
importRequest() importRequest()
elif(msg['cmd'] == 'newgame'): elif(msg['cmd'] == 'newgame'):
@ -431,16 +508,29 @@ def get_message(msg):
commitwi(msg['data']) commitwi(msg['data'])
elif(msg['cmd'] == 'aidgimport'): elif(msg['cmd'] == 'aidgimport'):
importAidgRequest(msg['data']) 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(): def setStartState():
emit('from_server', {'cmd': 'updatescreen', 'data': '<span>Welcome to <span class="color_cyan">KoboldAI Client</span>! You are running <span class="color_green">'+vars.model+'</span>.<br/>Please load a game or enter a prompt below to begin!</span>'}) emit('from_server', {'cmd': 'updatescreen', 'data': '<span>Welcome to <span class="color_cyan">KoboldAI Client</span>! You are running <span class="color_green">'+vars.model+'</span>.<br/>Please load a game or enter a prompt below to begin!</span>'})
emit('from_server', {'cmd': 'setgamestate', 'data': 'start'}) emit('from_server', {'cmd': 'setgamestate', 'data': 'start'})
#==================================================================# #==================================================================#
# # Transmit applicable settings to SocketIO to build UI sliders/toggles
#==================================================================# #==================================================================#
def sendsettings(): def sendsettings():
# Send settings for selected AI type # Send settings for selected AI type
@ -459,7 +549,7 @@ def sendsettings():
vars.formatoptns[frm["id"]] = False; vars.formatoptns[frm["id"]] = False;
#==================================================================# #==================================================================#
# # Take settings from vars and write them to client settings file
#==================================================================# #==================================================================#
def savesettings(): def savesettings():
# Build json to write # Build json to write
@ -482,7 +572,7 @@ def savesettings():
file.close() file.close()
#==================================================================# #==================================================================#
# # Read settings from client file JSON and send to vars
#==================================================================# #==================================================================#
def loadsettings(): def loadsettings():
if(path.exists("client.settings")): if(path.exists("client.settings")):
@ -513,7 +603,7 @@ def loadsettings():
file.close() file.close()
#==================================================================# #==================================================================#
# # Don't save settings unless 2 seconds have passed without modification
#==================================================================# #==================================================================#
@debounce(2) @debounce(2)
def settingschanged(): def settingschanged():
@ -521,7 +611,7 @@ def settingschanged():
savesettings() savesettings()
#==================================================================# #==================================================================#
# # Take input text from SocketIO and decide what to do with it
#==================================================================# #==================================================================#
def actionsubmit(data): def actionsubmit(data):
# Ignore new submissions if the AI is currently busy # Ignore new submissions if the AI is currently busy
@ -601,10 +691,12 @@ def calcsubmit(txt):
subtxt = vars.memory + winfo + anotetxt + vars.prompt subtxt = vars.memory + winfo + anotetxt + vars.prompt
lnsub = lnmem + lnwi + lnprompt + lnanote lnsub = lnmem + lnwi + lnprompt + lnanote
if(vars.model != "Colab"): if(not vars.model in ["Colab", "OAI"]):
generate(subtxt, lnsub+1, lnsub+vars.genamt) generate(subtxt, lnsub+1, lnsub+vars.genamt)
else: elif(vars.model == "Colab"):
sendtocolab(subtxt, lnsub+1, lnsub+vars.genamt) sendtocolab(subtxt, lnsub+1, lnsub+vars.genamt)
elif(vars.model == "OAI"):
oairequest(subtxt, lnsub+1, lnsub+vars.genamt)
else: else:
tokens = [] tokens = []
@ -643,23 +735,28 @@ def calcsubmit(txt):
# Prepend Memory, WI, and Prompt before action tokens # Prepend Memory, WI, and Prompt before action tokens
tokens = memtokens + witokens + prompttkns + tokens tokens = memtokens + witokens + prompttkns + tokens
# Send completed bundle to generator # Send completed bundle to generator
ln = len(tokens) ln = len(tokens)
if(vars.model != "Colab"): if(not vars.model in ["Colab", "OAI"]):
generate ( generate (
tokenizer.decode(tokens), tokenizer.decode(tokens),
ln+1, ln+1,
ln+vars.genamt ln+vars.genamt
) )
else: elif(vars.model == "Colab"):
sendtocolab( sendtocolab(
tokenizer.decode(tokens), tokenizer.decode(tokens),
ln+1, ln+1,
ln+vars.genamt ln+vars.genamt
) )
elif(vars.model == "OAI"):
oairequest(
tokenizer.decode(tokens),
ln+1,
ln+vars.genamt
)
# For InferKit web API # For InferKit web API
else: else:
@ -1151,6 +1248,55 @@ def ikrequest(txt):
emit('from_server', {'cmd': 'errmsg', 'data': errmsg}) emit('from_server', {'cmd': 'errmsg', 'data': errmsg})
set_aibusy(0) 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 # Forces UI to Play mode
#==================================================================# #==================================================================#
@ -1164,11 +1310,44 @@ def exitModes():
vars.mode = "play" 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")]) savpath = fileops.getsavepath(vars.savedir, "Save Story As", [("Json", "*.json")])
saveRequest(savpath)
#==================================================================#
# Save the story to specified path
#==================================================================#
def saveRequest(savpath):
if(savpath): if(savpath):
# Leave Edit/Memory mode before continuing # Leave Edit/Memory mode before continuing
exitModes() exitModes()
@ -1200,12 +1379,23 @@ def saveRequest():
finally: finally:
file.close() 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 # Load a stored story from a file
#==================================================================# #==================================================================#
def loadRequest(): def loadRequest(loadpath):
loadpath = fileops.getloadpath(vars.savedir, "Select Story File", [("Json", "*.json")])
if(loadpath): if(loadpath):
# Leave Edit/Memory mode before continuing # Leave Edit/Memory mode before continuing
exitModes() exitModes()
@ -1242,6 +1432,12 @@ def loadRequest():
file.close() file.close()
# Save path for save button
vars.savedir = loadpath
# Clear loadselect var
vars.loadselect = ""
# Refresh game screen # Refresh game screen
sendwi() sendwi()
refresh_story() refresh_story()
@ -1351,6 +1547,9 @@ def importgame():
# Clear import data # Clear import data
vars.importjs = {} vars.importjs = {}
# Reset current save
vars.savedir = getcwd()+"\stories"
# Refresh game screen # Refresh game screen
sendwi() sendwi()
refresh_story() refresh_story()
@ -1388,6 +1587,9 @@ def importAidgRequest(id):
}) })
num += 1 num += 1
# Reset current save
vars.savedir = getcwd()+"\stories"
# Refresh game screen # Refresh game screen
sendwi() sendwi()
refresh_story() refresh_story()
@ -1397,30 +1599,26 @@ def importAidgRequest(id):
# Starts a new story # Starts a new story
#==================================================================# #==================================================================#
def newGameRequest(): def newGameRequest():
# Ask for confirmation # Leave Edit/Memory mode before continuing
root = tk.Tk() exitModes()
root.attributes("-topmost", True)
confirm = tk.messagebox.askquestion("Confirm New Game", "Really start new Story?")
root.destroy()
if(confirm == "yes"): # Clear vars values
# Leave Edit/Memory mode before continuing vars.gamestarted = False
exitModes() vars.prompt = ""
vars.memory = ""
# Clear vars values vars.actions = []
vars.gamestarted = False
vars.prompt = "" vars.authornote = ""
vars.memory = "" vars.worldinfo = []
vars.actions = [] vars.lastact = ""
vars.savedir = getcwd()+"\stories" vars.lastctx = ""
vars.authornote = ""
vars.worldinfo = [] # Reset current save
vars.lastact = "" vars.savedir = getcwd()+"\stories"
vars.lastctx = ""
# Refresh game screen
# Refresh game screen sendwi()
sendwi() setStartState()
setStartState()
#==================================================================# #==================================================================#

View File

@ -1,5 +1,7 @@
import tkinter as tk import tkinter as tk
from tkinter import filedialog from tkinter import filedialog
from os import getcwd, listdir, path
import json
#==================================================================# #==================================================================#
# Generic Method for prompting for file path # Generic Method for prompting for file path
@ -50,4 +52,26 @@ def getdirpath(dir, title):
if(path != "" and path != None): if(path != "" and path != None):
return path return path
else: else:
return None 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")

View File

@ -1 +1 @@
pip install -r requirements.txt start cmd /k pip install -r requirements.txt

View File

@ -1 +1 @@
py -3 aiserver.py start cmd /k py -3 aiserver.py

View File

@ -9,6 +9,8 @@ var socket;
var connect_status; var connect_status;
var button_newgame; var button_newgame;
var button_save; var button_save;
var button_saveas;
var button_savetofile;
var button_load; var button_load;
var button_import; var button_import;
var button_impaidg; var button_impaidg;
@ -40,6 +42,18 @@ var aidgpopup;
var aidgpromptnum; var aidgpromptnum;
var aidg_accept; var aidg_accept;
var aidg_close; 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 // Key states
var shift_down = false; var shift_down = false;
@ -201,6 +215,7 @@ function hideWiDeleteConfirm(num) {
function highlightImportLine(ref) { function highlightImportLine(ref) {
$("#popupcontent > div").removeClass("popuplistselected"); $("#popupcontent > div").removeClass("popuplistselected");
ref.addClass("popuplistselected"); ref.addClass("popuplistselected");
enableButtons([popup_accept]);
} }
function enableButtons(refs) { function enableButtons(refs) {
@ -270,6 +285,7 @@ function popupShow(state) {
if(state) { if(state) {
popup.removeClass("hidden"); popup.removeClass("hidden");
popup.addClass("flex"); popup.addClass("flex");
disableButtons([popup_accept]);
} else { } else {
popup.removeClass("flex"); popup.removeClass("flex");
popup.addClass("hidden"); popup.addClass("hidden");
@ -385,6 +401,68 @@ function sendAidgImportRequest() {
aidgpromptnum.val(""); 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.length; i++) {
loadcontent.append("<div class=\"loadlistitem\" id=\"load"+i+"\" name=\""+ar[i].name+"\">\
<div>"+ar[i].name+"</div>\
<div>"+ar[i].actions+"</div>\
</div>");
$("#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 // READY/RUNTIME
//=================================================================// //=================================================================//
@ -392,40 +470,55 @@ function sendAidgImportRequest() {
$(document).ready(function(){ $(document).ready(function(){
// Bind UI references // Bind UI references
connect_status = $('#connectstatus'); connect_status = $('#connectstatus');
button_newgame = $('#btn_newgame'); button_newgame = $('#btn_newgame');
button_save = $('#btn_save'); button_save = $('#btn_save');
button_load = $('#btn_load'); button_saveas = $('#btn_saveas');
button_import = $("#btn_import"); button_savetofile = $('#btn_savetofile');
button_impaidg = $("#btn_impaidg"); button_load = $('#btn_load');
button_settings = $('#btn_settings'); button_loadfrfile = $('#btn_loadfromfile');
button_format = $('#btn_format'); button_import = $("#btn_import");
button_send = $('#btnsend'); button_impaidg = $("#btn_impaidg");
button_actedit = $('#btn_actedit'); button_settings = $('#btn_settings');
button_actmem = $('#btn_actmem'); button_format = $('#btn_format');
button_actback = $('#btn_actundo'); button_send = $('#btnsend');
button_actretry = $('#btn_actretry'); button_actedit = $('#btn_actedit');
button_delete = $('#btn_delete'); button_actmem = $('#btn_actmem');
button_actwi = $('#btn_actwi'); button_actback = $('#btn_actundo');
game_text = $('#gametext'); button_actretry = $('#btn_actretry');
input_text = $('#input_text'); button_delete = $('#btn_delete');
message_text = $('#messagefield'); button_actwi = $('#btn_actwi');
settings_menu = $("#settingsmenu"); game_text = $('#gametext');
format_menu = $('#formatmenu'); input_text = $('#input_text');
anote_menu = $('#anoterowcontainer'); message_text = $('#messagefield');
wi_menu = $('#wimenu'); settings_menu = $("#settingsmenu");
anote_input = $('#anoteinput'); format_menu = $('#formatmenu');
anote_labelcur = $('#anotecur'); anote_menu = $('#anoterowcontainer');
anote_slider = $('#anotedepth'); wi_menu = $('#wimenu');
popup = $("#popupcontainer"); anote_input = $('#anoteinput');
popup_title = $("#popuptitletext"); anote_labelcur = $('#anotecur');
popup_content = $("#popupcontent"); anote_slider = $('#anotedepth');
popup_accept = $("#btn_popupaccept"); popup = $("#popupcontainer");
popup_close = $("#btn_popupclose"); popup_title = $("#popuptitletext");
aidgpopup = $("#aidgpopupcontainer"); popup_content = $("#popupcontent");
aidgpromptnum = $("#aidgpromptnum"); popup_accept = $("#btn_popupaccept");
aidg_accept = $("#btn_aidgpopupaccept"); popup_close = $("#btn_popupclose");
aidg_close = $("#btn_aidgpopupclose"); 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 // Connect to SocketIO server
loc = window.document.location; loc = window.document.location;
@ -465,10 +558,14 @@ $(document).ready(function(){
hide([wi_menu, button_delete]); hide([wi_menu, button_delete]);
show([game_text, button_actedit, button_actmem, button_actwi, button_actback, button_actretry]); show([game_text, button_actedit, button_actmem, button_actwi, button_actback, button_actretry]);
hideMessage(); hideMessage();
hideWaitAnimation();
button_actedit.html("Edit"); button_actedit.html("Edit");
button_actmem.html("Memory"); button_actmem.html("Memory");
button_actwi.html("W Info"); button_actwi.html("W Info");
hideAidgPopup(); hideAidgPopup();
hideSaveAsPopup();
hideLoadPopup();
hideNewStoryPopup();
} }
} else if(msg.cmd == "editmode") { } else if(msg.cmd == "editmode") {
// Enable or Disable edit mode // Enable or Disable edit mode
@ -598,8 +695,20 @@ $(document).ready(function(){
} else if(msg.cmd == "requestwiitem") { } else if(msg.cmd == "requestwiitem") {
// Package WI contents and send back to server // Package WI contents and send back to server
returnWiList(msg.data); 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() { socket.on('disconnect', function() {
connect_status.html("<b>Lost connection...</b>"); connect_status.html("<b>Lost connection...</b>");
@ -632,22 +741,18 @@ $(document).ready(function(){
socket.send({'cmd': 'memory', 'data': ''}); socket.send({'cmd': 'memory', 'data': ''});
}); });
button_save.on("click", function(ev) { button_savetofile.on("click", function(ev) {
socket.send({'cmd': 'save', 'data': ''}); socket.send({'cmd': 'savetofile', 'data': ''});
}); });
button_load.on("click", function(ev) { button_loadfrfile.on("click", function(ev) {
socket.send({'cmd': 'load', 'data': ''}); socket.send({'cmd': 'loadfromfile', 'data': ''});
}); });
button_import.on("click", function(ev) { button_import.on("click", function(ev) {
socket.send({'cmd': 'import', 'data': ''}); socket.send({'cmd': 'import', 'data': ''});
}); });
button_newgame.on("click", function(ev) {
socket.send({'cmd': 'newgame', 'data': ''});
});
button_settings.on("click", function(ev) { button_settings.on("click", function(ev) {
$('#settingsmenu').slideToggle("slow"); $('#settingsmenu').slideToggle("slow");
}); });
@ -680,6 +785,58 @@ $(document).ready(function(){
sendAidgImportRequest(); 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 // Bind Enter button to submit
input_text.keydown(function (ev) { input_text.keydown(function (ev) {
if (ev.which == 13 && !shift_down) { if (ev.which == 13 && !shift_down) {
@ -705,5 +862,11 @@ $(document).ready(function(){
sendAidgImportRequest(); sendAidgImportRequest();
} }
}); });
saveasinput.keydown(function (ev) {
if (ev.which == 13 && saveasinput.val() != "") {
sendSaveAsRequest();
}
});
}); });

View File

@ -226,6 +226,43 @@ chunk {
color: #ffffff; 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 =================*/ /*================= Classes =================*/
.aidgpopupcontent { .aidgpopupcontent {
@ -381,6 +418,29 @@ chunk {
text-align: right; 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 { .navbar .navbar-nav .nav-link:hover {
border-radius: 5px; border-radius: 5px;
background-color: #98bcdb; background-color: #98bcdb;

View File

@ -6,13 +6,13 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="static/socket.io.min.js"></script> <script src="static/socket.io.min.js"></script>
<script src="static/application.js?ver=0.14.2"></script> <script src="static/application.js?ver=0.15.0g"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
<script src="static/bootstrap-toggle.min.js"></script> <script src="static/bootstrap-toggle.min.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
<link rel="stylesheet" href="static/bootstrap-toggle.min.css"> <link rel="stylesheet" href="static/bootstrap-toggle.min.css">
<link rel="stylesheet" href="static/custom.css?ver=0.14.2"> <link rel="stylesheet" href="static/custom.css?ver=0.15.0g">
</head> </head>
<body> <body>
<div class="container"> <div class="container">
@ -30,14 +30,23 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="#" id="btn_newgame">New Story</a> <a class="nav-link" href="#" id="btn_newgame">New Story</a>
</li> </li>
<li class="nav-item"> <li class="nav-item dropdown">
<a class="nav-link" href="#" id="btn_save">Save</a> <a class="nav-link dropdown-toggle" href="#" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Save</a>
</li> <div class="dropdown-menu">
<li class="nav-item"> <a class="dropdown-item" href="#" id="btn_save">Save</a>
<a class="nav-link" href="#" id="btn_load">Load</a> <a class="dropdown-item" href="#" id="btn_saveas">Save As</a>
<a class="dropdown-item" href="#" id="btn_savetofile">Save To File...</a>
</div>
</li> </li>
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Import...</a> <a class="nav-link dropdown-toggle" href="#" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Load</a>
<div class="dropdown-menu">
<a class="dropdown-item" href="#" id="btn_load">Load</a>
<a class="dropdown-item" href="#" id="btn_loadfromfile">Load From File...</a>
</div>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Import</a>
<div class="dropdown-menu"> <div class="dropdown-menu">
<a class="dropdown-item" href="#" id="btn_import">AI Dungeon Adventure</a> <a class="dropdown-item" href="#" id="btn_import">AI Dungeon Adventure</a>
<a class="dropdown-item" href="#" id="btn_impaidg">aidg.club Prompt</a> <a class="dropdown-item" href="#" id="btn_impaidg">aidg.club Prompt</a>
@ -159,5 +168,53 @@
</div> </div>
</div> </div>
</div> </div>
<div class="popupcontainer hidden" id="saveascontainer">
<div id="saveaspopup">
<div class="popuptitlebar">
<div class="popuptitletext">Enter Name For Save</div>
</div>
<div class="aidgpopupcontent">
<input class="form-control" type="text" placeholder="Save Name" id="savename">
</div>
<div class="hidden" id="saveasoverwrite">
<span>File already exists. Really overwrite?</span>
</div>
<div class="popupfooter">
<button type="button" class="btn btn-primary" id="btn_saveasaccept">Accept</button>
<button type="button" class="btn btn-primary" id="btn_saveasclose">Cancel</button>
</div>
</div>
</div>
<div class="popupcontainer hidden" id="loadcontainer">
<div id="loadpopup">
<div class="popuptitlebar">
<div class="popuptitletext">Select A Story To Load</div>
</div>
<div class="loadlistheader">
<div>Save Name</div>
<div># Actions</div>
</div>
<div id="loadlistcontent">
</div>
<div class="popupfooter">
<button type="button" class="btn btn-primary" id="btn_loadaccept">Load</button>
<button type="button" class="btn btn-primary" id="btn_loadclose">Cancel</button>
</div>
</div>
</div>
<div class="popupcontainer hidden" id="newgamecontainer">
<div id="nspopup">
<div class="popuptitlebar">
<div class="popuptitletext">Really Start A New Story?</div>
</div>
<div class="aidgpopuplistheader">
Unsaved data will be lost.
</div>
<div class="popupfooter">
<button type="button" class="btn btn-primary" id="btn_nsaccept">Accept</button>
<button type="button" class="btn btn-primary" id="btn_nsclose">Cancel</button>
</div>
</div>
</div>
</body> </body>
</html> </html>

View File

@ -80,9 +80,14 @@ def addsentencespacing(txt, vars):
if(lastchar == "." or lastchar == "!" or lastchar == "?" or lastchar == "," or lastchar == ";" or lastchar == ":"): if(lastchar == "." or lastchar == "!" or lastchar == "?" or lastchar == "," or lastchar == ";" or lastchar == ":"):
txt = " " + txt txt = " " + txt
return 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