mirror of
https://github.com/KoboldAI/KoboldAI-Client.git
synced 2025-02-26 00:17:41 +01:00
Merge branch 'united' into multi-gpu
This commit is contained in:
commit
a42b580027
60
aiserver.py
60
aiserver.py
@ -1,6 +1,6 @@
|
|||||||
#==================================================================#
|
#==================================================================#
|
||||||
# KoboldAI
|
# KoboldAI
|
||||||
# Version: 1.16.0
|
# Version: 1.16.2
|
||||||
# By: KoboldAIDev and the KoboldAI Community
|
# By: KoboldAIDev and the KoboldAI Community
|
||||||
#==================================================================#
|
#==================================================================#
|
||||||
|
|
||||||
@ -44,10 +44,11 @@ class colors:
|
|||||||
|
|
||||||
# AI models
|
# AI models
|
||||||
modellist = [
|
modellist = [
|
||||||
["Custom Neo (GPT-Neo / Converted GPT-J)", "NeoCustom", ""],
|
["Load a model from its directory", "NeoCustom", ""],
|
||||||
["Custom GPT-2 (eg CloverEdition)", "GPT2Custom", ""],
|
["Load an old GPT-2 model (eg CloverEdition)", "GPT2Custom", ""],
|
||||||
["GPT Neo 1.3B", "EleutherAI/gpt-neo-1.3B", "8GB"],
|
["GPT-Neo 1.3B", "EleutherAI/gpt-neo-1.3B", "8GB"],
|
||||||
["GPT Neo 2.7B", "EleutherAI/gpt-neo-2.7B", "16GB"],
|
["GPT-Neo 2.7B", "EleutherAI/gpt-neo-2.7B", "16GB"],
|
||||||
|
["GPT-J 6B (HF GIT Required)", "EleutherAI/gpt-j-6B", "24GB"],
|
||||||
["GPT-2", "gpt2", "1GB"],
|
["GPT-2", "gpt2", "1GB"],
|
||||||
["GPT-2 Med", "gpt2-medium", "2GB"],
|
["GPT-2 Med", "gpt2-medium", "2GB"],
|
||||||
["GPT-2 Large", "gpt2-large", "4GB"],
|
["GPT-2 Large", "gpt2-large", "4GB"],
|
||||||
@ -397,10 +398,15 @@ print("{0}OK!{1}".format(colors.GREEN, colors.END))
|
|||||||
if(not vars.model in ["InferKit", "Colab", "OAI", "ReadOnly"]):
|
if(not vars.model in ["InferKit", "Colab", "OAI", "ReadOnly"]):
|
||||||
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, GPTNeoModel, AutoModel
|
from transformers import pipeline, GPT2Tokenizer, GPT2LMHeadModel, GPTNeoForCausalLM, GPTNeoModel, AutoModelForCausalLM
|
||||||
|
|
||||||
# If custom GPT Neo model was chosen
|
# If custom GPT Neo model was chosen
|
||||||
if(vars.model == "NeoCustom"):
|
if(vars.model == "NeoCustom"):
|
||||||
|
model_config = open(vars.custmodpth + "/config.json", "r")
|
||||||
|
js = json.load(model_config)
|
||||||
|
if("model_type" in js):
|
||||||
|
model = vars.custmodpth
|
||||||
|
else:
|
||||||
model = GPTNeoForCausalLM.from_pretrained(vars.custmodpth)
|
model = GPTNeoForCausalLM.from_pretrained(vars.custmodpth)
|
||||||
tokenizer = GPT2Tokenizer.from_pretrained(vars.custmodpth)
|
tokenizer = GPT2Tokenizer.from_pretrained(vars.custmodpth)
|
||||||
# Is CUDA available? If so, use GPU, otherwise fall back to CPU
|
# Is CUDA available? If so, use GPU, otherwise fall back to CPU
|
||||||
@ -460,7 +466,7 @@ if(not vars.model in ["InferKit", "Colab", "OAI", "ReadOnly"]):
|
|||||||
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
|
import breakmodel
|
||||||
model = AutoModel.from_pretrained(vars.model)
|
model = AutoModelForCausalLM.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
|
||||||
model.half().to('cpu')
|
model.half().to('cpu')
|
||||||
@ -930,19 +936,21 @@ def settingschanged():
|
|||||||
#==================================================================#
|
#==================================================================#
|
||||||
# Take input text from SocketIO and decide what to do with it
|
# Take input text from SocketIO and decide what to do with it
|
||||||
#==================================================================#
|
#==================================================================#
|
||||||
def actionsubmit(data, actionmode=0):
|
def actionsubmit(data, actionmode=0, force_submit=False):
|
||||||
# Ignore new submissions if the AI is currently busy
|
# Ignore new submissions if the AI is currently busy
|
||||||
if(vars.aibusy):
|
if(vars.aibusy):
|
||||||
return
|
return
|
||||||
set_aibusy(1)
|
set_aibusy(1)
|
||||||
|
|
||||||
vars.recentback = False
|
vars.recentback = False
|
||||||
|
vars.recentedit = False
|
||||||
vars.actionmode = actionmode
|
vars.actionmode = actionmode
|
||||||
|
|
||||||
# "Action" mode
|
# "Action" mode
|
||||||
if(actionmode == 1):
|
if(actionmode == 1):
|
||||||
data = data.strip().lstrip('>')
|
data = data.strip().lstrip('>')
|
||||||
data = re.sub(r'\n+', ' ', data)
|
data = re.sub(r'\n+', ' ', data)
|
||||||
|
if(len(data)):
|
||||||
data = f"\n\n> {data}\n"
|
data = f"\n\n> {data}\n"
|
||||||
|
|
||||||
# If we're not continuing, store a copy of the raw input
|
# If we're not continuing, store a copy of the raw input
|
||||||
@ -950,13 +958,16 @@ def actionsubmit(data, actionmode=0):
|
|||||||
vars.lastact = data
|
vars.lastact = data
|
||||||
|
|
||||||
if(not vars.gamestarted):
|
if(not vars.gamestarted):
|
||||||
|
if(not force_submit and len(data.strip()) == 0):
|
||||||
|
set_aibusy(0)
|
||||||
|
return
|
||||||
# Start the game
|
# Start the game
|
||||||
vars.gamestarted = True
|
vars.gamestarted = True
|
||||||
# Save this first action as the prompt
|
# Save this first action as the prompt
|
||||||
vars.prompt = data
|
vars.prompt = data
|
||||||
if(not vars.noai):
|
if(not vars.noai):
|
||||||
# Clear the startup text from game screen
|
# Clear the startup text from game screen
|
||||||
emit('from_server', {'cmd': 'updatescreen', 'gamestarted': vars.gamestarted, 'data': 'Please wait, generating story...'}, broadcast=True)
|
emit('from_server', {'cmd': 'updatescreen', 'gamestarted': False, 'data': 'Please wait, generating story...'}, broadcast=True)
|
||||||
calcsubmit(data) # Run the first action through the generator
|
calcsubmit(data) # Run the first action through the generator
|
||||||
emit('from_server', {'cmd': 'scrolldown', 'data': ''}, broadcast=True)
|
emit('from_server', {'cmd': 'scrolldown', 'data': ''}, broadcast=True)
|
||||||
else:
|
else:
|
||||||
@ -970,6 +981,9 @@ def actionsubmit(data, actionmode=0):
|
|||||||
if(vars.actionmode == 0):
|
if(vars.actionmode == 0):
|
||||||
data = applyinputformatting(data)
|
data = applyinputformatting(data)
|
||||||
# Store the result in the Action log
|
# Store the result in the Action log
|
||||||
|
if(len(vars.prompt.strip()) == 0):
|
||||||
|
vars.prompt = data
|
||||||
|
else:
|
||||||
vars.actions.append(data)
|
vars.actions.append(data)
|
||||||
update_story_chunk('last')
|
update_story_chunk('last')
|
||||||
|
|
||||||
@ -999,7 +1013,9 @@ def actionretry(data):
|
|||||||
remove_story_chunk(last_key + 1)
|
remove_story_chunk(last_key + 1)
|
||||||
vars.genseqs = []
|
vars.genseqs = []
|
||||||
calcsubmit('')
|
calcsubmit('')
|
||||||
|
emit('from_server', {'cmd': 'scrolldown', 'data': ''}, broadcast=True)
|
||||||
vars.recentback = False
|
vars.recentback = False
|
||||||
|
vars.recentedit = False
|
||||||
elif(not vars.useprompt):
|
elif(not vars.useprompt):
|
||||||
emit('from_server', {'cmd': 'errmsg', 'data': "Please enable \"Always Add Prompt\" to retry with your prompt."})
|
emit('from_server', {'cmd': 'errmsg', 'data': "Please enable \"Always Add Prompt\" to retry with your prompt."})
|
||||||
|
|
||||||
@ -1282,6 +1298,9 @@ def genresult(genout):
|
|||||||
genout = applyoutputformatting(genout)
|
genout = applyoutputformatting(genout)
|
||||||
|
|
||||||
# Add formatted text to Actions array and refresh the game screen
|
# Add formatted text to Actions array and refresh the game screen
|
||||||
|
if(len(vars.prompt.strip()) == 0):
|
||||||
|
vars.prompt = genout
|
||||||
|
else:
|
||||||
vars.actions.append(genout)
|
vars.actions.append(genout)
|
||||||
update_story_chunk('last')
|
update_story_chunk('last')
|
||||||
emit('from_server', {'cmd': 'texteffect', 'data': vars.actions.get_last_key() if len(vars.actions) else 0}, broadcast=True)
|
emit('from_server', {'cmd': 'texteffect', 'data': vars.actions.get_last_key() if len(vars.actions) else 0}, broadcast=True)
|
||||||
@ -1476,7 +1495,7 @@ def update_story_chunk(idx: Union[int, str]):
|
|||||||
item = vars.acregex_ui.sub('<action>\\1</action>', item) # Add special formatting to adventure actions
|
item = vars.acregex_ui.sub('<action>\\1</action>', item) # Add special formatting to adventure actions
|
||||||
|
|
||||||
chunk_text = f'<chunk n="{idx}" id="n{idx}" tabindex="-1">{formatforhtml(item)}</chunk>'
|
chunk_text = f'<chunk n="{idx}" id="n{idx}" tabindex="-1">{formatforhtml(item)}</chunk>'
|
||||||
emit('from_server', {'cmd': 'updatechunk', 'data': {'index': idx, 'html': chunk_text, 'last': (idx == (vars.actions.get_last_key() if len(vars.actions) else 0))}}, broadcast=True)
|
emit('from_server', {'cmd': 'updatechunk', 'data': {'index': idx, 'html': chunk_text}}, broadcast=True)
|
||||||
|
|
||||||
|
|
||||||
#==================================================================#
|
#==================================================================#
|
||||||
@ -1548,6 +1567,7 @@ def editrequest(n):
|
|||||||
#
|
#
|
||||||
#==================================================================#
|
#==================================================================#
|
||||||
def editsubmit(data):
|
def editsubmit(data):
|
||||||
|
vars.recentedit = True
|
||||||
if(vars.editln == 0):
|
if(vars.editln == 0):
|
||||||
vars.prompt = data
|
vars.prompt = data
|
||||||
else:
|
else:
|
||||||
@ -1562,6 +1582,7 @@ def editsubmit(data):
|
|||||||
#
|
#
|
||||||
#==================================================================#
|
#==================================================================#
|
||||||
def deleterequest():
|
def deleterequest():
|
||||||
|
vars.recentedit = True
|
||||||
# Don't delete prompt
|
# Don't delete prompt
|
||||||
if(vars.editln == 0):
|
if(vars.editln == 0):
|
||||||
# Send error message
|
# Send error message
|
||||||
@ -1576,8 +1597,11 @@ def deleterequest():
|
|||||||
#
|
#
|
||||||
#==================================================================#
|
#==================================================================#
|
||||||
def inlineedit(chunk, data):
|
def inlineedit(chunk, data):
|
||||||
|
vars.recentedit = True
|
||||||
chunk = int(chunk)
|
chunk = int(chunk)
|
||||||
if(chunk == 0):
|
if(chunk == 0):
|
||||||
|
if(len(data.strip()) == 0):
|
||||||
|
return
|
||||||
vars.prompt = data
|
vars.prompt = data
|
||||||
else:
|
else:
|
||||||
vars.actions[chunk-1] = data
|
vars.actions[chunk-1] = data
|
||||||
@ -1590,6 +1614,7 @@ def inlineedit(chunk, data):
|
|||||||
#
|
#
|
||||||
#==================================================================#
|
#==================================================================#
|
||||||
def inlinedelete(chunk):
|
def inlinedelete(chunk):
|
||||||
|
vars.recentedit = True
|
||||||
chunk = int(chunk)
|
chunk = int(chunk)
|
||||||
# Don't delete prompt
|
# Don't delete prompt
|
||||||
if(chunk == 0):
|
if(chunk == 0):
|
||||||
@ -2094,7 +2119,18 @@ def loadRequest(loadpath):
|
|||||||
|
|
||||||
del vars.actions
|
del vars.actions
|
||||||
vars.actions = structures.KoboldStoryRegister()
|
vars.actions = structures.KoboldStoryRegister()
|
||||||
for s in js["actions"]:
|
actions = collections.deque(js["actions"])
|
||||||
|
|
||||||
|
if(len(vars.prompt.strip()) == 0):
|
||||||
|
while(len(actions)):
|
||||||
|
action = actions.popleft()
|
||||||
|
if(len(action.strip()) != 0):
|
||||||
|
vars.prompt = action
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
vars.gamestarted = False
|
||||||
|
if(vars.gamestarted):
|
||||||
|
for s in actions:
|
||||||
vars.actions.append(s)
|
vars.actions.append(s)
|
||||||
|
|
||||||
# Try not to break older save files
|
# Try not to break older save files
|
||||||
@ -2369,7 +2405,7 @@ def newGameRequest():
|
|||||||
def randomGameRequest(topic):
|
def randomGameRequest(topic):
|
||||||
newGameRequest()
|
newGameRequest()
|
||||||
vars.memory = "You generate the following " + topic + " story concept :"
|
vars.memory = "You generate the following " + topic + " story concept :"
|
||||||
actionsubmit("")
|
actionsubmit("", force_submit=True)
|
||||||
vars.memory = ""
|
vars.memory = ""
|
||||||
|
|
||||||
#==================================================================#
|
#==================================================================#
|
||||||
|
@ -11,7 +11,8 @@ dependencies:
|
|||||||
- python=3.8.*
|
- python=3.8.*
|
||||||
- cudatoolkit=11.1
|
- cudatoolkit=11.1
|
||||||
- tensorflow-gpu
|
- tensorflow-gpu
|
||||||
- transformers
|
|
||||||
- pip
|
- pip
|
||||||
|
- git
|
||||||
- pip:
|
- pip:
|
||||||
- flask-cloudflared
|
- flask-cloudflared
|
||||||
|
- git+https://github.com/huggingface/transformers#transformer
|
@ -69,11 +69,18 @@ var gamestarted = false;
|
|||||||
var editmode = false;
|
var editmode = false;
|
||||||
var connected = false;
|
var connected = false;
|
||||||
var newly_loaded = true;
|
var newly_loaded = true;
|
||||||
var current_editing_chunk = null;
|
var modified_chunks = new Set();
|
||||||
var chunk_conflict = false;
|
var empty_chunks = new Set();
|
||||||
|
var mutation_observer = null;
|
||||||
|
var gametext_bound = false;
|
||||||
|
var saved_prompt = "...";
|
||||||
|
var override_focusout = false;
|
||||||
var sman_allow_delete = false;
|
var sman_allow_delete = false;
|
||||||
var sman_allow_rename = false;
|
var sman_allow_rename = false;
|
||||||
|
|
||||||
|
// This is true iff [we're in macOS and the browser is Safari] or [we're in iOS]
|
||||||
|
var using_webkit_patch = null;
|
||||||
|
|
||||||
// Key states
|
// Key states
|
||||||
var shift_down = false;
|
var shift_down = false;
|
||||||
var do_clear_ent = false;
|
var do_clear_ent = false;
|
||||||
@ -400,7 +407,7 @@ function hideWaitAnimation() {
|
|||||||
|
|
||||||
function scrollToBottom() {
|
function scrollToBottom() {
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
$('#gamescreen').animate({scrollTop: $('#gamescreen').prop('scrollHeight')}, 500);
|
game_text.stop(true).animate({scrollTop: game_text.prop('scrollHeight')}, 500);
|
||||||
}, 5);
|
}, 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -462,6 +469,7 @@ function enterWiMode() {
|
|||||||
hide([button_actback, button_actmem, button_actretry, game_text]);
|
hide([button_actback, button_actmem, button_actretry, game_text]);
|
||||||
show([wi_menu]);
|
show([wi_menu]);
|
||||||
disableSendBtn();
|
disableSendBtn();
|
||||||
|
$("#gamescreen").addClass("wigamescreen");
|
||||||
}
|
}
|
||||||
|
|
||||||
function exitWiMode() {
|
function exitWiMode() {
|
||||||
@ -470,7 +478,7 @@ function exitWiMode() {
|
|||||||
hide([wi_menu]);
|
hide([wi_menu]);
|
||||||
show([button_actback, button_actmem, button_actretry, game_text]);
|
show([button_actback, button_actmem, button_actretry, game_text]);
|
||||||
enableSendBtn();
|
enableSendBtn();
|
||||||
scrollToBottom();
|
$("#gamescreen").removeClass("wigamescreen");
|
||||||
}
|
}
|
||||||
|
|
||||||
function returnWiList(ar) {
|
function returnWiList(ar) {
|
||||||
@ -489,7 +497,10 @@ function returnWiList(ar) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function dosubmit() {
|
function dosubmit() {
|
||||||
var txt = input_text.val();
|
var txt = input_text.val().replace(/\u00a0/g, " ");
|
||||||
|
if(!memorymode && !gamestarted && ((!adventure || !action_mode) && txt.trim().length == 0)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
socket.send({'cmd': 'submit', 'actionmode': adventure ? action_mode : 0, 'data': txt});
|
socket.send({'cmd': 'submit', 'actionmode': adventure ? action_mode : 0, 'data': txt});
|
||||||
if(memorymode) {
|
if(memorymode) {
|
||||||
memorytext = input_text.val();
|
memorytext = input_text.val();
|
||||||
@ -720,9 +731,6 @@ function setadventure(state) {
|
|||||||
|
|
||||||
function autofocus(event) {
|
function autofocus(event) {
|
||||||
if(connected) {
|
if(connected) {
|
||||||
if(event.target.tagName == "CHUNK") {
|
|
||||||
current_editing_chunk = event.target;
|
|
||||||
}
|
|
||||||
event.target.focus();
|
event.target.focus();
|
||||||
} else {
|
} else {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -733,72 +741,12 @@ function chunkOnKeyDown(event) {
|
|||||||
// Make escape commit the changes (Originally we had Enter here to but its not required and nicer for users if we let them type freely
|
// Make escape commit the changes (Originally we had Enter here to but its not required and nicer for users if we let them type freely
|
||||||
// You can add the following after 27 if you want it back to committing on enter : || (!event.shiftKey && event.keyCode == 13)
|
// You can add the following after 27 if you want it back to committing on enter : || (!event.shiftKey && event.keyCode == 13)
|
||||||
if(event.keyCode == 27) {
|
if(event.keyCode == 27) {
|
||||||
setTimeout(function () {
|
override_focusout = true;
|
||||||
event.target.blur();
|
game_text.blur();
|
||||||
}, 5);
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow left and right arrow keys (and backspace) to move between chunks
|
|
||||||
switch(event.keyCode) {
|
|
||||||
case 37: // left
|
|
||||||
case 39: // right
|
|
||||||
var old_range = getSelection().getRangeAt(0);
|
|
||||||
var old_range_start = old_range.startOffset;
|
|
||||||
var old_range_end = old_range.endOffset;
|
|
||||||
var old_range_ancestor = old_range.commonAncestorContainer;
|
|
||||||
var old_range_start_container = old_range.startContainer;
|
|
||||||
var old_range_end_container = old_range.endContainer;
|
|
||||||
setTimeout(function () {
|
|
||||||
// Wait a few milliseconds and check if the caret has moved
|
|
||||||
var new_selection = getSelection();
|
|
||||||
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) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 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
|
|
||||||
var chunk = document.activeElement;
|
|
||||||
switch(event.keyCode) {
|
|
||||||
case 37: // left
|
|
||||||
if((chunk = chunk.previousSibling) && chunk.tagName == "CHUNK") {
|
|
||||||
var range = document.createRange();
|
|
||||||
range.selectNodeContents(chunk);
|
|
||||||
range.collapse(false);
|
|
||||||
new_selection.removeAllRanges();
|
|
||||||
new_selection.addRange(range);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 39: // right
|
|
||||||
if((chunk = chunk.nextSibling) && chunk.tagName == "CHUNK") {
|
|
||||||
chunk.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 2);
|
|
||||||
return;
|
|
||||||
|
|
||||||
case 8: // backspace
|
|
||||||
var old_length = document.activeElement.innerText.length;
|
|
||||||
setTimeout(function () {
|
|
||||||
// Wait a few milliseconds and compare the chunk's length
|
|
||||||
if(old_length != document.activeElement.innerText.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// If it's the same, we're at the beginning of a chunk
|
|
||||||
if((chunk = document.activeElement.previousSibling) && chunk.tagName == "CHUNK") {
|
|
||||||
var range = document.createRange();
|
|
||||||
var selection = getSelection();
|
|
||||||
range.selectNodeContents(chunk);
|
|
||||||
range.collapse(false);
|
|
||||||
selection.removeAllRanges();
|
|
||||||
selection.addRange(range);
|
|
||||||
}
|
|
||||||
}, 2);
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't allow any edits if not connected to server
|
// Don't allow any edits if not connected to server
|
||||||
if(!connected) {
|
if(!connected) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -820,25 +768,6 @@ function chunkOnKeyDown(event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function submitEditedChunk(event) {
|
|
||||||
// Don't do anything if the current chunk hasn't been edited or if someone
|
|
||||||
// else overwrote it while you were busy lollygagging
|
|
||||||
if(current_editing_chunk === null || chunk_conflict) {
|
|
||||||
chunk_conflict = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var chunk = current_editing_chunk;
|
|
||||||
current_editing_chunk = null;
|
|
||||||
|
|
||||||
// Submit the edited chunk if it's not empty, otherwise delete it
|
|
||||||
if(chunk.innerText.length) {
|
|
||||||
socket.send({'cmd': 'inlineedit', 'chunk': chunk.getAttribute("n"), 'data': chunk.innerText.replace(/\u00a0/g, " ")});
|
|
||||||
} else {
|
|
||||||
socket.send({'cmd': 'inlinedelete', 'data': chunk.getAttribute("n")});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadStory(format) {
|
function downloadStory(format) {
|
||||||
var filename_without_extension = storyname !== null ? storyname : "untitled";
|
var filename_without_extension = storyname !== null ? storyname : "untitled";
|
||||||
|
|
||||||
@ -890,6 +819,226 @@ function downloadStory(format) {
|
|||||||
URL.revokeObjectURL(objectURL);
|
URL.revokeObjectURL(objectURL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildChunkSetFromNodeArray(nodes) {
|
||||||
|
var set = new Set();
|
||||||
|
for(var i = 0; i < nodes.length; i++) {
|
||||||
|
node = nodes[i];
|
||||||
|
while(node !== null && node.tagName !== "CHUNK") {
|
||||||
|
node = node.parentNode;
|
||||||
|
}
|
||||||
|
if(node === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
set.add(node.getAttribute("n"));
|
||||||
|
}
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedNodes() {
|
||||||
|
var range = rangy.getSelection().getRangeAt(0); // rangy is not a typo
|
||||||
|
var nodes = range.getNodes([1,3]);
|
||||||
|
nodes.push(range.startContainer);
|
||||||
|
nodes.push(range.endContainer);
|
||||||
|
return nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyChunkDeltas(nodes) {
|
||||||
|
var chunks = Array.from(buildChunkSetFromNodeArray(nodes));
|
||||||
|
for(var i = 0; i < chunks.length; i++) {
|
||||||
|
modified_chunks.add(chunks[i]);
|
||||||
|
}
|
||||||
|
setTimeout(function() {
|
||||||
|
var chunks = Array.from(modified_chunks);
|
||||||
|
var selected_chunks = buildChunkSetFromNodeArray(getSelectedNodes());
|
||||||
|
for(var i = 0; i < chunks.length; i++) {
|
||||||
|
var chunk = document.getElementById("n" + chunks[i]);
|
||||||
|
if(chunk && chunk.innerText.length != 0 && chunks[i] != '0') {
|
||||||
|
if(!selected_chunks.has(chunks[i])) {
|
||||||
|
modified_chunks.delete(chunks[i]);
|
||||||
|
socket.send({'cmd': 'inlineedit', 'chunk': chunks[i], 'data': chunk.innerText.replace(/\u00a0/g, " ")});
|
||||||
|
}
|
||||||
|
empty_chunks.delete(chunks[i]);
|
||||||
|
} else {
|
||||||
|
if(!selected_chunks.has(chunks[i])) {
|
||||||
|
modified_chunks.delete(chunks[i]);
|
||||||
|
socket.send({'cmd': 'inlineedit', 'chunk': chunks[i], 'data': ''});
|
||||||
|
}
|
||||||
|
empty_chunks.add(chunks[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncAllModifiedChunks(including_selected_chunks=false) {
|
||||||
|
var chunks = Array.from(modified_chunks);
|
||||||
|
var selected_chunks = buildChunkSetFromNodeArray(getSelectedNodes());
|
||||||
|
for(var i = 0; i < chunks.length; i++) {
|
||||||
|
if(including_selected_chunks || !selected_chunks.has(chunks[i])) {
|
||||||
|
modified_chunks.delete(chunks[i]);
|
||||||
|
var chunk = document.getElementById("n" + chunks[i]);
|
||||||
|
var data = chunk ? document.getElementById("n" + chunks[i]).innerText.replace(/\u00a0/g, " ") : "";
|
||||||
|
if(data.length == 0) {
|
||||||
|
empty_chunks.add(chunks[i]);
|
||||||
|
} else {
|
||||||
|
empty_chunks.delete(chunks[i]);
|
||||||
|
}
|
||||||
|
socket.send({'cmd': 'inlineedit', 'chunk': chunks[i], 'data': data});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function restorePrompt() {
|
||||||
|
if(game_text[0].firstChild && game_text[0].firstChild.nodeType === 3) {
|
||||||
|
saved_prompt = game_text[0].firstChild.textContent.replace(/\u00a0/g, " ");
|
||||||
|
unbindGametext();
|
||||||
|
game_text[0].innerText = "";
|
||||||
|
bindGametext();
|
||||||
|
}
|
||||||
|
if($("#n0").length) {
|
||||||
|
$("#n0").remove();
|
||||||
|
}
|
||||||
|
var prompt_chunk = document.createElement("chunk");
|
||||||
|
prompt_chunk.setAttribute("n", "0");
|
||||||
|
prompt_chunk.setAttribute("id", "n0");
|
||||||
|
prompt_chunk.setAttribute("tabindex", "-1");
|
||||||
|
prompt_chunk.innerText = saved_prompt;
|
||||||
|
unbindGametext();
|
||||||
|
game_text[0].prepend(prompt_chunk);
|
||||||
|
bindGametext();
|
||||||
|
modified_chunks.delete('0');
|
||||||
|
empty_chunks.delete('0');
|
||||||
|
socket.send({'cmd': 'inlineedit', 'chunk': '0', 'data': saved_prompt});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteEmptyChunks() {
|
||||||
|
var chunks = Array.from(empty_chunks);
|
||||||
|
for(var i = 0; i < chunks.length; i++) {
|
||||||
|
empty_chunks.delete(chunks[i]);
|
||||||
|
if(chunks[i] === "0") {
|
||||||
|
// Don't delete the prompt
|
||||||
|
restorePrompt();
|
||||||
|
} else {
|
||||||
|
socket.send({'cmd': 'inlinedelete', 'data': chunks[i]});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(modified_chunks.has('0')) {
|
||||||
|
modified_chunks.delete(chunks[i]);
|
||||||
|
socket.send({'cmd': 'inlineedit', 'chunk': chunks[i], 'data': document.getElementById("n0").innerText.replace(/\u00a0/g, " ")});
|
||||||
|
}
|
||||||
|
saved_prompt = $("#n0")[0].innerText.replace(/\u00a0/g, " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function highlightEditingChunks() {
|
||||||
|
var chunks = $('chunk.editing').toArray();
|
||||||
|
var selected_chunks = buildChunkSetFromNodeArray(getSelectedNodes());
|
||||||
|
for(var i = 0; i < chunks.length; i++) {
|
||||||
|
var chunk = chunks[i];
|
||||||
|
if(!selected_chunks.has(chunks[i].getAttribute("n"))) {
|
||||||
|
unbindGametext();
|
||||||
|
$(chunk).removeClass('editing');
|
||||||
|
bindGametext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chunks = Array.from(selected_chunks);
|
||||||
|
for(var i = 0; i < chunks.length; i++) {
|
||||||
|
var chunk = $("#n"+chunks[i]);
|
||||||
|
unbindGametext();
|
||||||
|
chunk.addClass('editing');
|
||||||
|
bindGametext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This gets run every time the text in a chunk is edited
|
||||||
|
// or a chunk is deleted
|
||||||
|
function chunkOnDOMMutate(mutations, observer) {
|
||||||
|
if(!gametext_bound || !allowedit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var nodes = [];
|
||||||
|
for(var i = 0; i < mutations.length; i++) {
|
||||||
|
var mutation = mutations[i];
|
||||||
|
nodes = nodes.concat(Array.from(mutation.addedNodes), Array.from(mutation.removedNodes));
|
||||||
|
nodes.push(mutation.target);
|
||||||
|
}
|
||||||
|
applyChunkDeltas(nodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This gets run every time you try to paste text into the editor
|
||||||
|
function chunkOnPaste(event) {
|
||||||
|
if(!gametext_bound) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// If possible, intercept paste events into the editor in order to always
|
||||||
|
// paste as plaintext
|
||||||
|
if(event.originalEvent.clipboardData && document.queryCommandSupported && document.execCommand && document.queryCommandSupported('insertText')) {
|
||||||
|
event.preventDefault();
|
||||||
|
document.execCommand('insertText', false, event.originalEvent.clipboardData.getData('text/plain'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This gets run every time the caret moves in the editor
|
||||||
|
function _chunkOnSelectionChange(event, do_blur_focus) {
|
||||||
|
if(!gametext_bound || !allowedit || override_focusout) {
|
||||||
|
override_focusout = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTimeout(function() {
|
||||||
|
syncAllModifiedChunks();
|
||||||
|
setTimeout(function() {
|
||||||
|
highlightEditingChunks();
|
||||||
|
// Attempt to prevent Chromium-based browsers on Android from
|
||||||
|
// scrolling away from the current selection
|
||||||
|
if(do_blur_focus && !using_webkit_patch) {
|
||||||
|
setTimeout(function() {
|
||||||
|
game_text.blur();
|
||||||
|
game_text.focus();
|
||||||
|
}, 144);
|
||||||
|
}
|
||||||
|
}, 2);
|
||||||
|
}, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function chunkOnSelectionChange(event) {
|
||||||
|
return _chunkOnSelectionChange(event, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function chunkOnKeyDownSelectionChange(event) {
|
||||||
|
return _chunkOnSelectionChange(event, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This gets run when you defocus the editor by clicking
|
||||||
|
// outside of the editor or by pressing escape or tab
|
||||||
|
function chunkOnFocusOut(event) {
|
||||||
|
if(!gametext_bound || !allowedit || event.target !== game_text[0]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTimeout(function() {
|
||||||
|
if(document.activeElement === game_text[0] || game_text[0].contains(document.activeElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
syncAllModifiedChunks(true);
|
||||||
|
setTimeout(function() {
|
||||||
|
var blurred = game_text[0] !== document.activeElement;
|
||||||
|
if(blurred) {
|
||||||
|
deleteEmptyChunks();
|
||||||
|
}
|
||||||
|
setTimeout(function() {
|
||||||
|
$("chunk").removeClass('editing');
|
||||||
|
}, 2);
|
||||||
|
}, 2);
|
||||||
|
}, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindGametext() {
|
||||||
|
mutation_observer.observe(game_text[0], {characterData: true, childList: true, subtree: true});
|
||||||
|
gametext_bound = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function unbindGametext() {
|
||||||
|
mutation_observer.disconnect();
|
||||||
|
gametext_bound = false;
|
||||||
|
}
|
||||||
|
|
||||||
//=================================================================//
|
//=================================================================//
|
||||||
// READY/RUNTIME
|
// READY/RUNTIME
|
||||||
//=================================================================//
|
//=================================================================//
|
||||||
@ -973,13 +1122,43 @@ $(document).ready(function(){
|
|||||||
format_menu.html("");
|
format_menu.html("");
|
||||||
wi_menu.html("");
|
wi_menu.html("");
|
||||||
// Set up "Allow Editing"
|
// Set up "Allow Editing"
|
||||||
$('body').on('input', autofocus).on('keydown', 'chunk', chunkOnKeyDown).on('focusout', 'chunk', submitEditedChunk);
|
$('body').on('input', autofocus);
|
||||||
$('#allowediting').prop('checked', allowedit).prop('disabled', false).change().on('change', function () {
|
$('#allowediting').prop('checked', allowedit).prop('disabled', false).change().off('change').on('change', function () {
|
||||||
if(allowtoggle) {
|
if(allowtoggle) {
|
||||||
allowedit = $(this).prop('checked')
|
allowedit = gamestarted && $(this).prop('checked');
|
||||||
$("chunk").attr('contenteditable', allowedit)
|
game_text.attr('contenteditable', allowedit);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// A simple feature detection test to determine whether the user interface
|
||||||
|
// is using WebKit (Safari browser's rendering engine) because WebKit
|
||||||
|
// requires special treatment to work correctly with the KoboldAI editor
|
||||||
|
if(using_webkit_patch === null) {
|
||||||
|
using_webkit_patch = (function() {
|
||||||
|
try {
|
||||||
|
var active_element = document.activeElement;
|
||||||
|
var c = document.createElement("chunk");
|
||||||
|
var t = document.createTextNode("KoboldAI");
|
||||||
|
c.appendChild(t);
|
||||||
|
game_text[0].appendChild(c);
|
||||||
|
var r = rangy.createRange();
|
||||||
|
r.setStart(t, 6);
|
||||||
|
r.collapse(true);
|
||||||
|
var s = rangy.getSelection();
|
||||||
|
s.removeAllRanges();
|
||||||
|
s.addRange(r);
|
||||||
|
game_text.blur();
|
||||||
|
game_text.focus();
|
||||||
|
var offset = rangy.getSelection().focusOffset;
|
||||||
|
c.removeChild(t);
|
||||||
|
game_text[0].removeChild(c);
|
||||||
|
document.activeElement.blur();
|
||||||
|
active_element.focus();
|
||||||
|
return offset !== 6;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
} else if(msg.cmd == "updatescreen") {
|
} else if(msg.cmd == "updatescreen") {
|
||||||
var _gamestarted = gamestarted;
|
var _gamestarted = gamestarted;
|
||||||
gamestarted = msg.gamestarted;
|
gamestarted = msg.gamestarted;
|
||||||
@ -987,13 +1166,16 @@ $(document).ready(function(){
|
|||||||
action_mode = 0;
|
action_mode = 0;
|
||||||
changemode();
|
changemode();
|
||||||
}
|
}
|
||||||
// Send game content to Game Screen
|
unbindGametext();
|
||||||
if(allowedit && document.activeElement.tagName == "CHUNK") {
|
allowedit = gamestarted && $("#allowediting").prop('checked');
|
||||||
chunk_conflict = true;
|
game_text.attr('contenteditable', allowedit);
|
||||||
}
|
modified_chunks = new Set();
|
||||||
|
empty_chunks = new Set();
|
||||||
game_text.html(msg.data);
|
game_text.html(msg.data);
|
||||||
// Make content editable if need be
|
bindGametext();
|
||||||
$('chunk').attr('contenteditable', allowedit);
|
if(gamestarted) {
|
||||||
|
saved_prompt = $("#n0")[0].innerText.replace(/\u00a0/g, " ");
|
||||||
|
}
|
||||||
// Scroll to bottom of text
|
// Scroll to bottom of text
|
||||||
if(newly_loaded) {
|
if(newly_loaded) {
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
@ -1004,28 +1186,26 @@ $(document).ready(function(){
|
|||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
} else if(msg.cmd == "updatechunk") {
|
} else if(msg.cmd == "updatechunk") {
|
||||||
hideMessage();
|
hideMessage();
|
||||||
const {index, html, last} = msg.data;
|
const {index, html} = msg.data;
|
||||||
const existingChunk = game_text.children(`#n${index}`)
|
const existingChunk = game_text.children(`#n${index}`)
|
||||||
const newChunk = $(html);
|
const newChunk = $(html);
|
||||||
|
unbindGametext();
|
||||||
if (existingChunk.length > 0) {
|
if (existingChunk.length > 0) {
|
||||||
// Update existing chunk
|
// Update existing chunk
|
||||||
existingChunk.before(newChunk);
|
existingChunk.before(newChunk);
|
||||||
existingChunk.remove();
|
existingChunk.remove();
|
||||||
} else {
|
} else if (!empty_chunks.has(index.toString())) {
|
||||||
// Append at the end
|
// Append at the end
|
||||||
game_text.append(newChunk);
|
game_text.append(newChunk);
|
||||||
}
|
}
|
||||||
newChunk.attr('contenteditable', allowedit);
|
bindGametext();
|
||||||
hide([$('#curtain')]);
|
hide([$('#curtain')]);
|
||||||
if(last) {
|
|
||||||
// Scroll to bottom of text if it's the last element
|
|
||||||
scrollToBottom();
|
|
||||||
}
|
|
||||||
} else if(msg.cmd == "removechunk") {
|
} else if(msg.cmd == "removechunk") {
|
||||||
hideMessage();
|
hideMessage();
|
||||||
let index = msg.data;
|
let index = msg.data;
|
||||||
// Remove the chunk
|
unbindGametext();
|
||||||
game_text.children(`#n${index}`).remove()
|
game_text.children(`#n${index}`).remove() // Remove the chunk
|
||||||
|
bindGametext();
|
||||||
hide([$('#curtain')]);
|
hide([$('#curtain')]);
|
||||||
} else if(msg.cmd == "setgamestate") {
|
} else if(msg.cmd == "setgamestate") {
|
||||||
// Enable or Disable buttons
|
// Enable or Disable buttons
|
||||||
@ -1253,6 +1433,30 @@ $(document).ready(function(){
|
|||||||
connect_status.addClass("color_orange");
|
connect_status.addClass("color_orange");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Register editing events
|
||||||
|
game_text.on('keydown',
|
||||||
|
chunkOnKeyDown
|
||||||
|
).on('paste',
|
||||||
|
chunkOnPaste
|
||||||
|
).on('click',
|
||||||
|
chunkOnSelectionChange
|
||||||
|
).on('keydown',
|
||||||
|
chunkOnKeyDownSelectionChange
|
||||||
|
).on('focusout',
|
||||||
|
chunkOnFocusOut
|
||||||
|
);
|
||||||
|
mutation_observer = new MutationObserver(chunkOnDOMMutate);
|
||||||
|
|
||||||
|
// This is required for the editor to work correctly in Firefox on desktop
|
||||||
|
// because the gods of HTML and JavaScript say so
|
||||||
|
$(document.body).on('focusin', function(event) {
|
||||||
|
setTimeout(function() {
|
||||||
|
if(document.activeElement !== game_text[0] && game_text[0].contains(document.activeElement)) {
|
||||||
|
game_text[0].focus();
|
||||||
|
}
|
||||||
|
}, 2);
|
||||||
|
});
|
||||||
|
|
||||||
// Bind actions to UI buttons
|
// Bind actions to UI buttons
|
||||||
button_send.on("click", function(ev) {
|
button_send.on("click", function(ev) {
|
||||||
dosubmit();
|
dosubmit();
|
||||||
|
@ -11,12 +11,12 @@ chunk {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
chunk[contenteditable="true"]:focus, chunk[contenteditable="true"]:focus * {
|
chunk.editing, chunk.editing * {
|
||||||
color: #cdf !important;
|
color: #cdf !important;
|
||||||
font-weight: normal !important;
|
font-weight: normal !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
chunk, chunk * {
|
#gametext, chunk, chunk * {
|
||||||
outline: 0px solid transparent;
|
outline: 0px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,16 +69,19 @@ chunk, chunk * {
|
|||||||
#gamescreen {
|
#gamescreen {
|
||||||
height: 490px;
|
height: 490px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
padding: 10px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
overflow-y: scroll;
|
|
||||||
background-color: #262626;
|
background-color: #262626;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
font-size: 12pt;
|
font-size: 12pt;
|
||||||
font-family: "Helvetica";
|
font-family: "Helvetica";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#gamescreen.wigamescreen {
|
||||||
|
padding: 10px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
#gamescreen span {
|
#gamescreen span {
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
}
|
}
|
||||||
@ -87,6 +90,8 @@ chunk, chunk * {
|
|||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
padding: 10px;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
#seqselmenu {
|
#seqselmenu {
|
||||||
|
11
static/rangy-core.min.js
vendored
Normal file
11
static/rangy-core.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -6,13 +6,14 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<script src="static/jquery-3.6.0.min.js"></script>
|
<script src="static/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.15.0g"></script>
|
<script src="static/application.js?ver=1.16.2d"></script>
|
||||||
<script src="static/bootstrap.min.js"></script>
|
<script src="static/bootstrap.min.js"></script>
|
||||||
<script src="static/bootstrap-toggle.min.js"></script>
|
<script src="static/bootstrap-toggle.min.js"></script>
|
||||||
|
<script src="static/rangy-core.min.js"></script>
|
||||||
|
|
||||||
<link rel="stylesheet" href="static/bootstrap.min.css">
|
<link rel="stylesheet" href="static/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.15.0g">
|
<link rel="stylesheet" href="static/custom.css?ver=1.16.2a">
|
||||||
<link rel="stylesheet" href="static/open-iconic-bootstrap.min.css">
|
<link rel="stylesheet" href="static/open-iconic-bootstrap.min.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -81,7 +82,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="layer-container">
|
<div class="layer-container">
|
||||||
<div class="layer-bottom row" id="gamescreen">
|
<div class="layer-bottom row" id="gamescreen">
|
||||||
<span id="gametext"><p>...</p></span>
|
<span id="gametext" contenteditable="true"><p>...</p></span>
|
||||||
<div class="hidden" id="wimenu">
|
<div class="hidden" id="wimenu">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user