diff --git a/aiserver.py b/aiserver.py index 7ec35f79..da1602ce 100644 --- a/aiserver.py +++ b/aiserver.py @@ -106,6 +106,7 @@ class vars: tfs = 1.0 # Default generator tfs (tail-free sampling) numseqs = 1 # Number of sequences to ask the generator to create gamestarted = False # Whether the game has started (disables UI elements) + gamesaved = True # Whether or not current game is saved serverstarted = False # Whether or not the Flask server has started prompt = "" # Prompt memory = "" # Text submitted to memory field @@ -1647,6 +1648,7 @@ def lua_is_custommodel(): # #==================================================================# def execute_inmod(): + setgamesaved(False) vars.lua_logname = ... vars.lua_edited = set() vars.lua_deleted = set() @@ -1666,6 +1668,7 @@ def execute_genmod(): vars.lua_koboldbridge.execute_genmod() def execute_outmod(): + setgamesaved(False) emit('from_server', {'cmd': 'hidemsg', 'data': ''}, broadcast=True) try: tpool.execute(vars.lua_koboldbridge.execute_outmod) @@ -1733,7 +1736,7 @@ def do_connect(): print("{0}Client connected!{1}".format(colors.GREEN, colors.END)) emit('from_server', {'cmd': 'setchatname', 'data': vars.chatname}) emit('from_server', {'cmd': 'setanotetemplate', 'data': vars.authornotetemplate}) - emit('from_server', {'cmd': 'connected', 'smandelete': vars.smandelete, 'smanrename': vars.smanrename}) + emit('from_server', {'cmd': 'connected', 'smandelete': vars.smandelete, 'smanrename': vars.smanrename, 'modelname': getmodelname()}) if(vars.remote): emit('from_server', {'cmd': 'runs_remotely'}) if(vars.allowsp): @@ -1773,6 +1776,8 @@ def do_connect(): elif(vars.mode == "wi"): emit('from_server', {'cmd': 'wimode', 'data': 'true'}) + emit('from_server', {'cmd': 'gamesaved', 'data': vars.gamesaved}, broadcast=True) + #==================================================================# # Event triggered when browser SocketIO sends data to the server #==================================================================# @@ -1937,6 +1942,7 @@ def get_message(msg): togglewimode() elif(msg['cmd'] == 'wiinit'): if(int(msg['data']) < len(vars.worldinfo)): + setgamesaved(False) vars.worldinfo[msg['data']]["init"] = True addwiitem(folder_uid=msg['folder']) elif(msg['cmd'] == 'wifolderinit'): @@ -1951,17 +1957,22 @@ def get_message(msg): deletewifolder(msg['data']) elif(msg['cmd'] == 'wiexpand'): assert 0 <= int(msg['data']) < len(vars.worldinfo) + setgamesaved(False) emit('from_server', {'cmd': 'wiexpand', 'data': msg['data']}, broadcast=True) elif(msg['cmd'] == 'wiexpandfolder'): assert 0 <= int(msg['data']) < len(vars.worldinfo) + setgamesaved(False) emit('from_server', {'cmd': 'wiexpandfolder', 'data': msg['data']}, broadcast=True) elif(msg['cmd'] == 'wifoldercollapsecontent'): + setgamesaved(False) vars.wifolders_d[msg['data']]['collapsed'] = True emit('from_server', {'cmd': 'wifoldercollapsecontent', 'data': msg['data']}, broadcast=True) elif(msg['cmd'] == 'wifolderexpandcontent'): + setgamesaved(False) vars.wifolders_d[msg['data']]['collapsed'] = False emit('from_server', {'cmd': 'wifolderexpandcontent', 'data': msg['data']}, broadcast=True) elif(msg['cmd'] == 'wiupdate'): + setgamesaved(False) num = int(msg['num']) fields = ("key", "keysecondary", "content", "comment") for field in fields: @@ -1969,6 +1980,7 @@ def get_message(msg): vars.worldinfo[num][field] = msg['data'][field] emit('from_server', {'cmd': 'wiupdate', 'num': msg['num'], 'data': {field: vars.worldinfo[num][field] for field in fields}}, broadcast=True) elif(msg['cmd'] == 'wifolderupdate'): + setgamesaved(False) uid = int(msg['uid']) fields = ("name", "collapsed") for field in fields: @@ -1976,15 +1988,19 @@ def get_message(msg): vars.wifolders_d[uid][field] = msg['data'][field] emit('from_server', {'cmd': 'wifolderupdate', 'uid': msg['uid'], 'data': {field: vars.wifolders_d[uid][field] for field in fields}}, broadcast=True) elif(msg['cmd'] == 'wiselon'): + setgamesaved(False) vars.worldinfo[msg['data']]["selective"] = True emit('from_server', {'cmd': 'wiselon', 'data': msg['data']}, broadcast=True) elif(msg['cmd'] == 'wiseloff'): + setgamesaved(False) vars.worldinfo[msg['data']]["selective"] = False emit('from_server', {'cmd': 'wiseloff', 'data': msg['data']}, broadcast=True) elif(msg['cmd'] == 'wiconstanton'): + setgamesaved(False) vars.worldinfo[msg['data']]["constant"] = True emit('from_server', {'cmd': 'wiconstanton', 'data': msg['data']}, broadcast=True) elif(msg['cmd'] == 'wiconstantoff'): + setgamesaved(False) vars.worldinfo[msg['data']]["constant"] = False emit('from_server', {'cmd': 'wiconstantoff', 'data': msg['data']}, broadcast=True) elif(msg['cmd'] == 'sendwilist'): @@ -2280,6 +2296,15 @@ def settingschanged(): print("{0}Saving settings!{1}".format(colors.GREEN, colors.END)) savesettings() +#==================================================================# +# Set value of gamesaved +#==================================================================# +def setgamesaved(gamesaved): + assert type(gamesaved) is bool + if(gamesaved != vars.gamesaved): + emit('from_server', {'cmd': 'gamesaved', 'data': gamesaved}, broadcast=True) + vars.gamesaved = gamesaved + #==================================================================# # Take input text from SocketIO and decide what to do with it #==================================================================# @@ -3331,6 +3356,7 @@ def inlineedit(chunk, data): if(chunk-1 in vars.actions): vars.actions[chunk-1] = data + setgamesaved(False) update_story_chunk(chunk) emit('from_server', {'cmd': 'texteffect', 'data': chunk}, broadcast=True) emit('from_server', {'cmd': 'editmode', 'data': 'false'}, broadcast=True) @@ -3350,6 +3376,7 @@ def inlinedelete(chunk): else: if(chunk-1 in vars.actions): del vars.actions[chunk-1] + setgamesaved(False) remove_story_chunk(chunk) emit('from_server', {'cmd': 'editmode', 'data': 'false'}, broadcast=True) @@ -3419,6 +3446,7 @@ def addwifolder(): # the WI entry with UID dst #==================================================================# def movewiitem(dst, src): + setgamesaved(False) if(vars.worldinfo_u[src]["folder"] is not None): for i, e in enumerate(vars.wifolders_u[vars.worldinfo_u[src]["folder"]]): if(e is vars.worldinfo_u[src]): @@ -3440,6 +3468,7 @@ def movewiitem(dst, src): # the WI folder with UID dst #==================================================================# def movewifolder(dst, src): + setgamesaved(False) vars.wifolders_l.remove(src) if(dst is None): # If dst is None, that means we should move src to be the last folder @@ -3663,6 +3692,8 @@ def memsubmit(data): emit('from_server', {'cmd': 'setinputtext', 'data': data}, broadcast=True) # Maybe check for length at some point # For now just send it to storage + if(data != vars.memory): + setgamesaved(False) vars.memory = data vars.mode = "play" emit('from_server', {'cmd': 'memmode', 'data': 'false'}, broadcast=True) @@ -3677,6 +3708,8 @@ def anotesubmit(data, template=""): assert type(data) is str and type(template) is str # Maybe check for length at some point # For now just send it to storage + if(data != vars.authornote): + setgamesaved(False) vars.authornote = data if(vars.authornotetemplate != template): @@ -3964,6 +3997,7 @@ def saveRequest(savpath): filename = filename[:-5] vars.laststory = filename emit('from_server', {'cmd': 'setstoryname', 'data': vars.laststory}, broadcast=True) + setgamesaved(True) print("{0}Story saved to {1}!{2}".format(colors.GREEN, path.basename(savpath), colors.END)) #==================================================================# @@ -4113,6 +4147,7 @@ def loadRequest(loadpath, filename=None): _filename = filename[:-5] vars.laststory = _filename emit('from_server', {'cmd': 'setstoryname', 'data': vars.laststory}, broadcast=True) + setgamesaved(True) sendwi() emit('from_server', {'cmd': 'setmemory', 'data': vars.memory}, broadcast=True) emit('from_server', {'cmd': 'setanote', 'data': vars.authornote}, broadcast=True) @@ -4157,7 +4192,8 @@ def spRequest(filename): tensor = np.float32(tensor) assert not np.isinf(tensor).any() and not np.isnan(tensor).any() - vars.sp_length = tensor.shape[0] + vars.sp_length = tensor.shape[-2] + vars.spmeta["n_tokens"] = vars.sp_length if(vars.model in ("TPUMeshTransformerGPTJ",)): rows = tensor.shape[0] @@ -4319,6 +4355,7 @@ def importgame(): # Refresh game screen vars.laststory = None emit('from_server', {'cmd': 'setstoryname', 'data': vars.laststory}, broadcast=True) + setgamesaved(False) sendwi() emit('from_server', {'cmd': 'setmemory', 'data': vars.memory}, broadcast=True) emit('from_server', {'cmd': 'setanote', 'data': vars.authornote}, broadcast=True) @@ -4399,6 +4436,7 @@ def importAidgRequest(id): # Refresh game screen vars.laststory = None emit('from_server', {'cmd': 'setstoryname', 'data': vars.laststory}, broadcast=True) + setgamesaved(False) sendwi() emit('from_server', {'cmd': 'setmemory', 'data': vars.memory}, broadcast=True) emit('from_server', {'cmd': 'setanote', 'data': vars.authornote}, broadcast=True) @@ -4456,6 +4494,7 @@ def wiimportrequest(): print("{0}".format(vars.worldinfo[0])) # Refresh game screen + setgamesaved(False) sendwi() #==================================================================# @@ -4488,6 +4527,7 @@ def newGameRequest(): # Refresh game screen vars.laststory = None emit('from_server', {'cmd': 'setstoryname', 'data': vars.laststory}, broadcast=True) + setgamesaved(True) sendwi() emit('from_server', {'cmd': 'setmemory', 'data': vars.memory}, broadcast=True) emit('from_server', {'cmd': 'setanote', 'data': vars.authornote}, broadcast=True) @@ -4501,6 +4541,7 @@ def randomGameRequest(topic, memory=""): vars.recentrng = topic vars.recentrngm = memory newGameRequest() + setgamesaved(False) _memory = memory if(len(memory) > 0): _memory = memory.rstrip() + "\n\n" diff --git a/fileops.py b/fileops.py index e5697553..50f1e94f 100644 --- a/fileops.py +++ b/fileops.py @@ -161,6 +161,7 @@ def getspfiles(model_dimension: int): ob = {} z.close() ob["filename"] = file + ob["n_tokens"] = shape[-2] lst.append(ob) return lst diff --git a/static/application.js b/static/application.js index e8a3202f..14d7de37 100644 --- a/static/application.js +++ b/static/application.js @@ -93,6 +93,8 @@ var sman_allow_rename = false; var allowsp = false; var remote = false; var gamestate = ""; +var gamesaved = true; +var modelname = null; // This is true iff [we're in macOS and the browser is Safari] or [we're in iOS] var using_webkit_patch = true; @@ -169,6 +171,23 @@ function addSetting(ob) { } } +function refreshTitle() { + var title = gamesaved ? "" : "\u2731 "; + if(storyname !== null) { + title += storyname + " \u2014 "; + } + title += "KoboldAI Client"; + if(modelname !== null) { + title += " (" + modelname + ")"; + } + document.title = title; +} + +function setGameSaved(state) { + gamesaved = !!state; + refreshTitle(); +} + function addFormat(ob) { // Check if we need to make a new column for this button if(formatcount == 0) { @@ -972,11 +991,14 @@ function buildSPList(ar) { showSPPopup(); ar.push({filename: '', name: "[None]"}) for(var i = 0; i < ar.length; i++) { - var supported = !ar[i].supported + var author = !ar[i].author ? '' - : Object.prototype.toString.call(ar[i].supported) === "[object Array]" - ? "[" + ar[i].supported.join(', ') + "]" - : "[" + ar[i].supported.toString() + "]"; + : ar[i].author.constructor === Array + ? ar[i].author.join(', ') + : ar[i].author; + var n_tokens = !ar[i].n_tokens || !Number.isSafeInteger(ar[i].n_tokens) || ar[i].n_tokens < 1 + ? '' + : "(" + ar[i].n_tokens + " tokens)"; var filename = ar[i].filename.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/(?=\r|\n)\r?\n?/g, '
'); var name = ar[i].name || ar[i].filename; name = name.length > 120 ? name.slice(0, 117) + '...' : name; @@ -992,7 +1014,7 @@ function buildSPList(ar) { \
\
"+desc+"
\ -
"+supported+"
\ +
" + author + "
" + n_tokens + "
\
\ \ "); @@ -1108,7 +1130,7 @@ function updateSPStatItems(items) { stat_sp.closest(".statusicon").removeClass("active"); stat_spactive.html(""); } else { - stat_sp.html("Active soft prompt:"); + stat_sp.html("Active soft prompt (" + items[key].n_tokens + " tokens):"); stat_sp.closest(".statusicon").addClass("active"); stat_spactive.html((items[key].name || key)+" <"+key+">"); } @@ -1777,6 +1799,10 @@ $(document).ready(function(){ sman_allow_delete = msg.hasOwnProperty("smandelete") && msg.smandelete; sman_allow_rename = msg.hasOwnProperty("smanrename") && msg.smanrename; connected = true; + if(msg.hasOwnProperty("modelname")) { + modelname = msg.modelname; + } + refreshTitle(); connect_status.html("Connected to KoboldAI Process!"); connect_status.removeClass("color_orange"); connect_status.addClass("color_green"); @@ -1908,6 +1934,7 @@ $(document).ready(function(){ } } else if(msg.cmd == "setstoryname") { storyname = msg.data; + refreshTitle(); } else if(msg.cmd == "editmode") { // Enable or Disable edit mode if(msg.data == "true") { @@ -2138,6 +2165,8 @@ $(document).ready(function(){ } else if(msg.cmd == "saveas") { // Show Save As prompt showSaveAsPopup(); + } else if(msg.cmd == "gamesaved") { + setGameSaved(msg.data); } else if(msg.cmd == "hidesaveas") { // Hide Save As prompt hideSaveAsPopup(); @@ -2534,8 +2563,12 @@ $(document).ready(function(){ } }); + $([input_text, anote_input, $("#gamescreen")]).map($.fn.toArray).on("input", function() { + setGameSaved(false); + }); + $(window).on("beforeunload", function() { - if(gamestarted || memorytext.length > 0 || $("#anoteinput").val().length > 0 || $(".wilistitem").length > 1) { + if(!gamesaved) { return true; } }); diff --git a/static/custom.css b/static/custom.css index ce9c23d4..da30c6ed 100644 --- a/static/custom.css +++ b/static/custom.css @@ -1312,6 +1312,7 @@ body.connected .popupfooter, .popupfooter.always-available { .splistitemsub { color: #ba9; + text-align: right; } .splistitem:hover { diff --git a/templates/index.html b/templates/index.html index f4d67f83..be53a03d 100644 --- a/templates/index.html +++ b/templates/index.html @@ -9,7 +9,7 @@ - + @@ -17,7 +17,7 @@ - +