This commit is contained in:
ebolam
2022-12-05 13:53:17 -05:00
9 changed files with 1094 additions and 254 deletions

View File

@@ -7,6 +7,7 @@
# External packages
from dataclasses import dataclass
import shutil
import eventlet
eventlet.monkey_patch(all=True, thread=False, os=False)
import os, inspect
@@ -509,6 +510,9 @@ class ImportBuffer:
refresh_story()
import_buffer = ImportBuffer()
with open("data/genres.json", "r") as file:
genre_list = json.load(file)
# Set logging level to reduce chatter from Flask
import logging
@@ -7232,28 +7236,34 @@ def loadfromfile():
def loadRequest(loadpath, filename=None):
logger.debug("Load Request")
logger.debug("Called from {}".format(inspect.stack()[1].function))
if not loadpath:
return
if os.path.isdir(loadpath):
if not valid_v3_story(loadpath):
raise RuntimeError(f"Tried to load {loadpath}, a non-save directory.")
loadpath = os.path.join(loadpath, "story.json")
start_time = time.time()
if(loadpath):
# Leave Edit/Memory mode before continuing
exitModes()
# Read file contents into JSON object
start_time = time.time()
if(isinstance(loadpath, str)):
with open(loadpath, "r") as file:
js = json.load(file)
if(filename is None):
filename = path.basename(loadpath)
else:
js = loadpath
if(filename is None):
filename = "untitled.json"
js['v1_loadpath'] = loadpath
js['v1_filename'] = filename
logger.debug("Loading JSON data took {}s".format(time.time()-start_time))
loadJSON(js)
logger.debug("Time to load story: {}s".format(time.time()-start_time))
# Leave Edit/Memory mode before continuing
exitModes()
# Read file contents into JSON object
start_time = time.time()
if(isinstance(loadpath, str)):
with open(loadpath, "r") as file:
js = json.load(file)
if(filename is None):
filename = path.basename(loadpath)
else:
js = loadpath
if(filename is None):
filename = "untitled.json"
js['v1_loadpath'] = loadpath
js['v1_filename'] = filename
logger.debug("Loading JSON data took {}s".format(time.time()-start_time))
loadJSON(js)
def loadJSON(json_text_or_dict):
logger.debug("Loading JSON Story")
@@ -7925,6 +7935,31 @@ def upload_file(data):
f.write(data['data'])
get_files_folders(session['current_folder'])
@app.route("/upload_kai_story/<string:file_name>", methods=["POST"])
@logger.catch
def UI_2_upload_kai_story(file_name: str):
assert "/" not in file_name
raw_folder_name = file_name.replace(".kaistory", "")
folder_path = path.join("stories", raw_folder_name)
disambiguator = 0
while path.exists(folder_path):
disambiguator += 1
folder_path = path.join("stories", f"{raw_folder_name} ({disambiguator})")
buffer = BytesIO()
dat = request.get_data()
with open("debug.zip", "wb") as file:
file.write(dat)
buffer.write(dat)
with zipfile.ZipFile(buffer, "r") as zipf:
zipf.extractall(folder_path)
return ":)"
@socketio.on('popup_change_folder')
@logger.catch
def popup_change_folder(data):
@@ -7965,38 +8000,46 @@ def popup_rename(data):
@logger.catch
def popup_rename_story(data):
if 'popup_renameable' not in session:
print("Someone is trying to rename a file in your server. Blocked.")
logger.warning("Someone is trying to rename a file in your server. Blocked.")
return
if not session['popup_renameable']:
print("Someone is trying to rename a file in your server. Blocked.")
logger.warning("Someone is trying to rename a file in your server. Blocked.")
return
if session['popup_jailed_dir'] is None:
#if we're using a v2 file we can't just rename the file as the story name is in the file
with open(data['file'], 'r') as f:
json_data = json.load(f)
if 'story_name' in json_data:
json_data['story_name'] = data['new_name']
new_filename = os.path.join(os.path.dirname(os.path.abspath(data['file'])), data['new_name']+".json")
os.remove(data['file'])
with open(new_filename, "w") as f:
json.dump(json_data, f)
get_files_folders(os.path.dirname(data['file']))
elif session['popup_jailed_dir'] in data['file']:
#if we're using a v2 file we can't just rename the file as the story name is in the file
with open(data['file'], 'r') as f:
json_data = json.load(f)
if 'story_name' in json_data:
json_data['story_name'] = data['new_name']
new_filename = os.path.join(os.path.dirname(os.path.abspath(data['file'])), data['new_name']+".json")
os.remove(data['file'])
with open(new_filename, "w") as f:
json.dump(json_data, f)
get_files_folders(os.path.dirname(data['file']))
if session['popup_jailed_dir'] and session["popup_jailed_dir"] not in data["file"]:
logger.warning("User is trying to rename files in your server outside the jail. Blocked. Jailed Dir: {} Requested Dir: {}".format(session['popup_jailed_dir'], data['file']))
return
path = data["file"]
new_name = data["new_name"]
json_path = path
is_v3 = False
# Handle directory for v3 save
if os.path.isdir(path):
if not valid_v3_story(path):
return
is_v3 = True
json_path = os.path.join(path, "story.json")
#if we're using a v2 file we can't just rename the file as the story name is in the file
with open(json_path, 'r') as f:
json_data = json.load(f)
if 'story_name' in json_data:
json_data['story_name'] = new_name
# For v3 we move the directory, not the json file.
if is_v3:
target = os.path.join(os.path.dirname(path), new_name)
shutil.move(path, target)
with open(os.path.join(target, "story.json"), "w") as file:
json.dump(json_data, file)
else:
print("User is trying to rename files in your server outside the jail. Blocked. Jailed Dir: {} Requested Dir: {}".format(session['popup_jailed_dir'], data['file']))
new_filename = os.path.join(os.path.dirname(os.path.abspath(data['file'])), new_name+".json")
os.remove(data['file'])
with open(new_filename, "w") as f:
json.dump(json_data, f)
get_files_folders(os.path.dirname(path))
@socketio.on('popup_delete')
@logger.catch
@@ -8159,10 +8202,12 @@ def get_files_folders(starting_folder):
folders = []
files = []
base_path = os.path.abspath(starting_folder).replace("\\", "/")
if advanced_sort is not None:
files_to_check = advanced_sort(base_path, desc=desc)
else:
files_to_check = get_files_sorted(base_path, sort, desc=desc)
for item in files_to_check:
item_full_path = os.path.join(base_path, item).replace("\\", "/")
if hasattr(os.stat(item_full_path), "st_file_attributes"):
@@ -8179,8 +8224,15 @@ def get_files_folders(starting_folder):
extra_parameters = extra_parameter_function(item_full_path, item, valid_selection)
if (show_hidden and hidden) or not hidden:
if os.path.isdir(os.path.join(base_path, item)):
folders.append([True, item_full_path, item, valid_selection, extra_parameters])
if os.path.isdir(item_full_path):
folders.append([
# While v3 saves are directories, we should not show them as such.
not valid_v3_story(item_full_path),
item_full_path,
item,
valid_selection,
extra_parameters
])
else:
if hide_extention:
item = ".".join(item.split(".")[:-1])
@@ -8320,18 +8372,55 @@ def UI_2_save_story(data):
else:
#We have an ack that it's OK to save over the file if one exists
koboldai_vars.save_story()
def directory_to_zip_data(directory: str, overrides: Optional[dict]) -> bytes:
overrides = overrides or {}
buffer = BytesIO()
with zipfile.ZipFile(buffer, "w") as zipf:
for root, _, files in os.walk(directory):
for file in files:
p = os.path.join(root, file)
z_path = os.path.join(*p.split(os.path.sep)[2:])
if z_path in overrides:
continue
zipf.write(p, z_path)
for path, contents in overrides.items():
zipf.writestr(path, contents)
return buffer.getvalue()
#==================================================================#
# Save story to json
#==================================================================#
@app.route("/json")
@app.route("/story_download")
@logger.catch
def UI_2_save_to_json():
def UI_2_download_story():
save_exists = path.exists(koboldai_vars.save_paths.base)
if koboldai_vars.gamesaved and save_exists:
# Disk is up to date; download from disk
data = directory_to_zip_data(koboldai_vars.save_paths.base)
elif save_exists:
# We aren't up to date but we are saved; patch what disk gives us
data = directory_to_zip_data(
koboldai_vars.save_paths.base,
{"story.json": koboldai_vars.to_json("story_settings")}
)
else:
# We are not saved; send json in zip from memory
buffer = BytesIO()
with zipfile.ZipFile(buffer, "w") as zipf:
zipf.writestr("story.json", koboldai_vars.to_json("story_settings"))
data = buffer.getvalue()
return Response(
koboldai_vars.to_json('story_settings'),
mimetype="application/json",
headers={"Content-disposition":
"attachment; filename={}.v2.json".format(koboldai_vars.story_name)})
data,
mimetype="application/octet-stream",
headers={"Content-disposition": f"attachment; filename={koboldai_vars.story_name}.kaistory"}
)
#==================================================================#
@@ -8523,6 +8612,11 @@ def get_story_listing_data(item_full_path, item, valid_selection):
if not valid_selection:
return [title, action_count, last_loaded]
if os.path.isdir(item_full_path):
if not valid_v3_story(item_full_path):
return [title, action_count, last_loaded]
item_full_path = os.path.join(item_full_path, "story.json")
with open(item_full_path, 'rb') as f:
parse_event = ijson.parse(f)
@@ -8574,38 +8668,68 @@ def get_story_listing_data(item_full_path, item, valid_selection):
return [title, action_count, last_loaded]
@logger.catch
def valid_story(file):
if file.endswith(".json"):
try:
valid = False
with open(file, 'rb') as f:
parser = ijson.parse(f)
for prefix, event, value in parser:
if prefix == 'memory':
valid=True
break
except:
pass
return valid
def valid_story(path: str):
if os.path.isdir(path):
return valid_v3_story(path)
if not path.endswith(".json"):
return False
try:
with open(path, 'rb') as file:
parser = ijson.parse(file)
for prefix, event, value in parser:
if prefix == 'memory':
return True
except:
pass
return False
@logger.catch
def valid_v3_story(path: str) -> bool:
if not os.path.exists(path): return False
if not os.path.isdir(path): return False
if not os.path.exists(os.path.join(path, "story.json")): return False
return True
@logger.catch
def story_sort(base_path, desc=False):
files = {}
for file in os.scandir(path=base_path):
if file.name.endswith(".json"):
filename = os.path.join(base_path, file.name).replace("\\", "/")
if os.path.getsize(filename) < 2*1024*1024: #2MB
with open(filename, "r") as f:
try:
js = json.load(f)
if 'story_name' in js and js['story_name'] in koboldai_vars.story_loads:
files[file.name] = datetime.datetime.strptime(koboldai_vars.story_loads[js['story_name']], "%m/%d/%Y, %H:%M:%S")
else:
files[file.name] = datetime.datetime.fromtimestamp(file.stat().st_mtime)
except:
pass
if file.is_dir():
if not valid_v3_story(file.path):
continue
story_path = os.path.join(file.path, "story.json")
story_stat = os.stat(story_path)
if os.path.getsize(story_path) < 2*1024*1024: #2MB
with open(story_path, "r") as f:
j = json.load(f)
if j.get("story_name") in koboldai_vars.story_loads:
files[file.name] = datetime.datetime.strptime(koboldai_vars.story_loads[j["story_name"]], "%m/%d/%Y, %H:%M:%S")
else:
files[file.name] = datetime.datetime.fromtimestamp(story_stat.st_mtime)
else:
files[file.name] = datetime.datetime.fromtimestamp(file.stat().st_mtime)
files[file.name] = datetime.datetime.fromtimestamp(story_stat.st_mtime)
continue
if not file.name.endswith(".json"):
continue
filename = os.path.join(base_path, file.name).replace("\\", "/")
if os.path.getsize(filename) < 2*1024*1024: #2MB
with open(filename, "r") as f:
try:
js = json.load(f)
if 'story_name' in js and js['story_name'] in koboldai_vars.story_loads:
files[file.name] = datetime.datetime.strptime(koboldai_vars.story_loads[js['story_name']], "%m/%d/%Y, %H:%M:%S")
else:
files[file.name] = datetime.datetime.fromtimestamp(file.stat().st_mtime)
except:
pass
else:
files[file.name] = datetime.datetime.fromtimestamp(file.stat().st_mtime)
return [key[0] for key in sorted(files.items(), key=lambda kv: (kv[1], kv[0]), reverse=desc)]
@@ -8843,6 +8967,7 @@ def UI_2_set_wi_image(uid):
else:
# Otherwise assign image
koboldai_vars.worldinfo_v2.image_store[uid] = data
koboldai_vars.gamesaved = False
return ":)"
@app.route("/get_wi_image/<int(signed=True):uid>", methods=["GET"])
@@ -9189,8 +9314,10 @@ def UI_2_generate_image_from_story(data):
#get latest action
if len(koboldai_vars.actions) > 0:
action = koboldai_vars.actions[-1]
action_id = len(koboldai_vars.actions) - 1
else:
action = koboldai_vars.prompt
action_id = -1
#Get matching world info entries
keys = []
for wi in koboldai_vars.worldinfo_v2:
@@ -9231,64 +9358,122 @@ def UI_2_generate_image_from_story(data):
keys = [summarize(text, max_length=max_length)]
logger.debug("Text from summarizer: {}".format(keys[0]))
generate_story_image(", ".join(keys), art_guide=art_guide)
prompt = ", ".join(keys)
generate_story_image(
", ".join([part for part in [prompt, art_guide] if part]),
file_prefix=f"action_{action_id}",
display_prompt=prompt,
log_data={"actionId": action_id}
)
@socketio.on("generate_image_from_prompt")
@logger.catch
def UI_2_generate_image_from_prompt(prompt: str):
eventlet.sleep(0)
generate_story_image(prompt)
generate_story_image(prompt, file_prefix="prompt", generation_type="direct_prompt")
def generate_story_image(prompt: str, art_guide: str = "") -> None:
def log_image_generation(
prompt: str,
display_prompt: str,
file_name: str,
generation_type: str,
other_data: Optional[dict] = None
) -> None:
# In the future it might be nice to have some UI where you can search past
# generations or something like that
db_path = os.path.join(koboldai_vars.save_paths.generated_images, "db.json")
try:
with open(db_path, "r") as file:
j = json.load(file)
except FileNotFoundError:
j = []
if not isinstance(j, list):
logger.warning("Image database is corrupted! Will not add new entry.")
return
log_data = {
"prompt": prompt,
"fileName": file_name,
"type": generation_type or None,
"displayPrompt": display_prompt
}
log_data.update(other_data or {})
j.append(log_data)
with open(db_path, "w") as file:
json.dump(j, file)
def generate_story_image(
prompt: str,
file_prefix: str = "image",
generation_type: str = "",
display_prompt: Optional[str] = None,
log_data: Optional[dict] = None
) -> None:
# This function is a wrapper around generate_image() that integrates the
# result with the story (read: puts it in the corner of the screen).
if not display_prompt:
display_prompt = prompt
koboldai_vars.picture_prompt = display_prompt
start_time = time.time()
koboldai_vars.generating_image = True
b64_data = generate_image(prompt, art_guide=art_guide)
image = generate_image(prompt)
koboldai_vars.generating_image = False
if not image:
return
if os.path.exists(koboldai_vars.save_paths.generated_images):
# Only save image if this is a saved story
file_name = f"{file_prefix}_{int(time.time())}.png"
image.save(os.path.join(koboldai_vars.save_paths.generated_images, file_name))
log_image_generation(prompt, display_prompt, file_name, generation_type, log_data)
logger.debug("Time to Generate Image {}".format(time.time()-start_time))
koboldai_vars.picture = b64_data
koboldai_vars.picture_prompt = prompt
koboldai_vars.generating_image = False
buffer = BytesIO()
image.save(buffer, format="JPEG")
b64_data = base64.b64encode(buffer.getvalue()).decode("ascii")
def generate_image(prompt: str, art_guide: str = "") -> Optional[str]:
koboldai_vars.picture = b64_data
def generate_image(prompt: str) -> Optional[Image.Image]:
if koboldai_vars.img_gen_priority == 4:
# Check if stable-diffusion-webui API option selected and use that if found.
return text2img_api(prompt, art_guide=art_guide)
return text2img_api(prompt)
elif ((not koboldai_vars.hascuda or not os.path.exists("models/stable-diffusion-v1-4")) and koboldai_vars.img_gen_priority != 0) or koboldai_vars.img_gen_priority == 3:
# If we don't have a GPU, use horde if we're allowed to
return text2img_horde(prompt, art_guide=art_guide)
return text2img_horde(prompt)
memory = torch.cuda.get_device_properties(0).total_memory
# We aren't being forced to use horde, so now let's figure out if we should use local
if memory - torch.cuda.memory_reserved(0) >= 6000000000:
# We have enough vram, just do it locally
return text2img_local(prompt, art_guide=art_guide)
return text2img_local(prompt)
elif memory > 6000000000 and koboldai_vars.img_gen_priority <= 1:
# We could do it locally by swapping the model out
print("Could do local or online")
return text2img_horde(prompt, art_guide=art_guide)
return text2img_horde(prompt)
elif koboldai_vars.img_gen_priority != 0:
return text2img_horde(prompt, art_guide=art_guide)
return text2img_horde(prompt)
raise RuntimeError("Unable to decide image generation backend. Please report this.")
@logger.catch
def text2img_local(prompt,
art_guide="",
filename="new.png"):
def text2img_local(prompt: str) -> Optional[Image.Image]:
start_time = time.time()
logger.debug("Generating Image")
koboldai_vars.aibusy = True
koboldai_vars.generating_image = True
from diffusers import StableDiffusionPipeline
import base64
from io import BytesIO
if koboldai_vars.image_pipeline is None:
pipe = tpool.execute(StableDiffusionPipeline.from_pretrained, "CompVis/stable-diffusion-v1-4", revision="fp16", torch_dtype=torch.float16, cache="models/stable-diffusion-v1-4").to("cuda")
else:
@@ -9301,9 +9486,6 @@ def text2img_local(prompt,
with autocast("cuda"):
return pipe(prompt, num_inference_steps=num_inference_steps).images[0]
image = tpool.execute(get_image, pipe, prompt, num_inference_steps=koboldai_vars.img_gen_steps)
buffered = BytesIO()
image.save(buffered, format="JPEG")
img_str = base64.b64encode(buffered.getvalue()).decode('ascii')
logger.debug("time to generate: {}".format(time.time() - start_time))
start_time = time.time()
if koboldai_vars.keep_img_gen_in_memory:
@@ -9314,61 +9496,52 @@ def text2img_local(prompt,
koboldai_vars.image_pipeline = None
del pipe
torch.cuda.empty_cache()
koboldai_vars.generating_image = False
koboldai_vars.aibusy = False
logger.debug("time to unload: {}".format(time.time() - start_time))
return img_str
return image
@logger.catch
def text2img_horde(prompt,
art_guide = "",
filename = "story_art.png"):
def text2img_horde(prompt: str) -> Optional[Image.Image]:
logger.debug("Generating Image using Horde")
koboldai_vars.generating_image = True
final_submit_dict = {
"prompt": "{}, {}".format(prompt, art_guide),
"prompt": prompt,
"trusted_workers": False,
"models": [
"stable_diffusion"
],
"params": {
"n":1,
"n": 1,
"nsfw": True,
"sampler_name": "k_euler_a",
"karras": True,
"cfg_scale": koboldai_vars.img_gen_cfg_scale,
"steps":koboldai_vars.img_gen_steps,
"width":512,
"height":512}
"steps": koboldai_vars.img_gen_steps,
"width": 512,
"height": 512
}
}
cluster_headers = {'apikey': koboldai_vars.sh_apikey if koboldai_vars.sh_apikey != '' else "0000000000",}
logger.debug(final_submit_dict)
submit_req = requests.post('https://stablehorde.net/api/v2/generate/sync', json = final_submit_dict, headers=cluster_headers)
if submit_req.ok:
results = submit_req.json()
for iter in range(len(results['generations'])):
b64img = results['generations'][iter]["img"]
base64_bytes = b64img.encode('utf-8')
img_bytes = base64.b64decode(base64_bytes)
img = Image.open(BytesIO(img_bytes))
if len(results) > 1:
final_filename = f"{iter}_{filename}"
else:
final_filename = filename
img.save(final_filename)
logger.debug("Saved Image")
koboldai_vars.generating_image = False
return(b64img)
else:
koboldai_vars.generating_image = False
submit_req = requests.post('https://stablehorde.net/api/v2/generate/sync', json=final_submit_dict, headers=cluster_headers)
if not submit_req.ok:
logger.error(submit_req.text)
return
results = submit_req.json()
if len(results["generations"]) > 1:
logger.warning(f"Got too many generations, discarding extras. Got {len(results['generations'])}, expected 1.")
b64img = results["generations"][0]["img"]
base64_bytes = b64img.encode("utf-8")
img_bytes = base64.b64decode(base64_bytes)
img = Image.open(BytesIO(img_bytes))
return img
@logger.catch
def text2img_api(prompt, art_guide=""):
def text2img_api(prompt, art_guide="") -> Image.Image:
logger.debug("Generating Image using Local SD-WebUI API")
koboldai_vars.generating_image = True
#The following list are valid properties with their defaults, to add/modify in final_imgen_params. Will refactor configuring values into UI element in future.
@@ -9402,7 +9575,7 @@ def text2img_api(prompt, art_guide=""):
#"override_settings": {},
#"sampler_index": "Euler"
final_imgen_params = {
"prompt": ", ".join(filter(bool, [prompt, art_guide])),
"prompt": prompt,
"n_iter": 1,
"width": 512,
"height": 512,
@@ -9451,7 +9624,7 @@ def text2img_api(prompt, art_guide=""):
show_error_notification("SD Web API Failure", "SD Web API returned no images", do_log=True)
return None
return base64_image
return Image.open(BytesIO(base64.b64decode(base64_image)))
@socketio.on("clear_generated_image")
@logger.catch
@@ -9630,6 +9803,15 @@ def UI_2_privacy_mode(data):
koboldai_vars.privacy_mode = False
#==================================================================#
# Genres
#==================================================================#
@app.route("/genre_data.json", methods=["GET"])
def UI_2_get_applicable_genres():
return Response(json.dumps({
"list": genre_list,
"init": koboldai_vars.genres,
}))
#==================================================================#
# Soft Prompt Tuning
#==================================================================#
@socketio.on("create_new_softprompt")
@@ -9701,7 +9883,7 @@ def UI_2_test_match():
@logger.catch
def UI_2_audio():
action_id = int(request.args['id']) if 'id' in request.args else len(koboldai_vars.actions)
filename="stories/{}/{}.ogg".format(koboldai_vars.story_id, action_id)
filename = os.path.join(koboldai_vars.save_paths.generated_audio, f"{action_id}.ogg")
if not os.path.exists(filename):
koboldai_vars.actions.gen_audio(action_id)
return send_file(

307
data/genres.json Normal file
View File

@@ -0,0 +1,307 @@
[
"Absurdist",
"Action & Adventure",
"Adaptations & Pastiche",
"African American & Black/General",
"African American & Black/Christian",
"African American & Black/Erotica",
"African American & Black/Historical",
"African American & Black/Mystery & Detective",
"African American & Black/Urban & Street Lit",
"African American & Black/Women",
"Alternative History",
"Amish & Mennonite",
"Animals",
"Anthologies (multiple authors)",
"Asian American",
"Biographical",
"Buddhist",
"Christian/General",
"Christian/Biblical",
"Christian/Classic & Allegory",
"Christian/Collections & Anthologies",
"Christian/Contemporary",
"Christian/Fantasy",
"Christian/Futuristic",
"Christian/Historical",
"Christian/Romance/General",
"Christian/Romance/Historical",
"Christian/Romance/Suspense",
"Christian/Suspense",
"Christian/Western",
"City Life",
"Classics",
"Coming of Age",
"Crime",
"Cultural Heritage",
"Disabilities & Special Needs",
"Disaster",
"Dystopian",
"Epistolary",
"Erotica/General",
"Erotica/BDSM",
"Erotica/Collections & Anthologies",
"Erotica/Historical",
"Erotica/LGBTQ+/General",
"Erotica/LGBTQ+/Bisexual",
"Erotica/LGBTQ+/Gay",
"Erotica/LGBTQ+/Lesbian",
"Erotica/LGBTQ+/Transgender",
"Erotica/Science Fiction, Fantasy & Horror",
"Fairy Tales, Folk Tales, Legends & Mythology",
"Family Life/General",
"Family Life/Marriage & Divorce",
"Family Life/Siblings",
"Fantasy/General",
"Fantasy/Action & Adventure",
"Fantasy/Arthurian",
"Fantasy/Collections & Anthologies",
"Fantasy/Contemporary",
"Fantasy/Dark Fantasy",
"Fantasy/Dragons & Mythical Creatures",
"Fantasy/Epic",
"Fantasy/Gaslamp",
"Fantasy/Historical",
"Fantasy/Humorous",
"Fantasy/Military",
"Fantasy/Paranormal",
"Fantasy/Romance",
"Fantasy/Urban",
"Feminist",
"Friendship",
"Ghost",
"Gothic",
"Hispanic & Latino",
"Historical/General",
"Historical/Ancient",
"Historical/Civil War Era",
"Historical/Colonial America & Revolution",
"Historical/Medieval",
"Historical/Renaissance",
"Historical/World War I",
"Historical/World War II",
"Holidays",
"Horror",
"Humorous/General",
"Humorous/Black Humor",
"Indigenous",
"Jewish",
"Legal",
"LGBTQ+/General",
"LGBTQ+/Bisexual",
"LGBTQ+/Gay",
"LGBTQ+/Lesbian",
"LGBTQ+/Transgender",
"Literary",
"LitRPG (Literary Role-Playing Game)",
"Magical Realism",
"Mashups",
"Media Tie-In",
"Medical",
"Multiple Timelines",
"Muslim",
"Mystery & Detective/General",
"Mystery & Detective/Amateur Sleuth",
"Mystery & Detective/Collections & Anthologies",
"Mystery & Detective/Cozy/General",
"Mystery & Detective/Cozy/Animals",
"Mystery & Detective/Cozy/Crafts",
"Mystery & Detective/Cozy/Culinary",
"Mystery & Detective/Cozy/Holidays & Vacation",
"Mystery & Detective/Cozy/Paranormal",
"Mystery & Detective/Hard-Boiled",
"Mystery & Detective/Historical",
"Mystery & Detective/International Crime & Mystery",
"Mystery & Detective/Jewish",
"Mystery & Detective/Police Procedural",
"Mystery & Detective/Private Investigators",
"Mystery & Detective/Traditional",
"Mystery & Detective/Women Sleuths",
"Nature & the Environment",
"Noir",
"Occult & Supernatural",
"Own Voices",
"Political",
"Psychological",
"Religious",
"Romance/General",
"Romance/Action & Adventure",
"Romance/African American & Black",
"Romance/Billionaires",
"Romance/Clean & Wholesome",
"Romance/Collections & Anthologies",
"Romance/Contemporary",
"Romance/Erotic",
"Romance/Fantasy",
"Romance/Firefighters",
"Romance/Historical/General",
"Romance/Historical/American",
"Romance/Historical/Ancient World",
"Romance/Historical/Gilded Age",
"Romance/Historical/Medieval",
"Romance/Historical/Regency",
"Romance/Historical/Renaissance",
"Romance/Historical/Scottish",
"Romance/Historical/Tudor",
"Romance/Historical/20th Century",
"Romance/Historical/Victorian",
"Romance/Historical/Viking",
"Romance/Holiday",
"Romance/Later in Life",
"Romance/LGBTQ+/General",
"Romance/LGBTQ+/Bisexual",
"Romance/LGBTQ+/Gay",
"Romance/LGBTQ+/Lesbian",
"Romance/LGBTQ+/Transgender",
"Romance/Medical",
"Romance/Military",
"Romance/Multicultural & Interracial",
"Romance/New Adult",
"Romance/Paranormal/General",
"Romance/Paranormal/Shifters",
"Romance/Paranormal/Vampires",
"Romance/Paranormal/Witches",
"Romance/Police & Law Enforcement",
"Romance/Polyamory",
"Romance/Rock Stars",
"Romance/Romantic Comedy",
"Romance/Royalty",
"Romance/Science Fiction",
"Romance/Sports",
"Romance/Suspense",
"Romance/Time Travel",
"Romance/Western",
"Romance/Workplace",
"Sagas",
"Satire",
"Science Fiction/General",
"Science Fiction/Action & Adventure",
"Science Fiction/Alien Contact",
"Science Fiction/Apocalyptic & Post-Apocalyptic",
"Science Fiction/Collections & Anthologies",
"Science Fiction/Crime & Mystery",
"Science Fiction/Cyberpunk",
"Science Fiction/Genetic Engineering",
"Science Fiction/Hard Science Fiction",
"Science Fiction/Humorous",
"Science Fiction/Military",
"Science Fiction/Space Exploration",
"Science Fiction/Space Opera",
"Science Fiction/Steampunk",
"Science Fiction/Time Travel",
"Sea Stories",
"Short Stories (single author)",
"Small Town & Rural",
"Southern",
"Sports",
"Superheroes",
"Thrillers/General",
"Thrillers/Crime",
"Thrillers/Domestic",
"Thrillers/Espionage",
"Thrillers/Historical",
"Thrillers/Legal",
"Thrillers/Medical",
"Thrillers/Military",
"Thrillers/Political",
"Thrillers/Psychological",
"Thrillers/Supernatural",
"Thrillers/Suspense",
"Thrillers/Technological",
"Thrillers/Terrorism",
"Urban & Street Lit",
"Visionary & Metaphysical",
"War & Military",
"Westerns",
"Women",
"World Literature/Africa/General",
"World Literature/Africa/East Africa",
"World Literature/Africa/Nigeria",
"World Literature/Africa/Southern Africa",
"World Literature/Africa/West Africa",
"World Literature/American/General",
"World Literature/American/Colonial & Revolutionary Periods",
"World Literature/American/19th Century",
"World Literature/American/20th Century",
"World Literature/American/21st Century",
"World Literature/Argentina",
"World Literature/Asia (General)",
"World Literature/Australia",
"World Literature/Austria",
"World Literature/Brazil",
"World Literature/Canada/General",
"World Literature/Canada/Colonial & 19th Century",
"World Literature/Canada/20th Century",
"World Literature/Canada/21st Century",
"World Literature/Caribbean & West Indies",
"World Literature/Central America",
"World Literature/Chile",
"World Literature/China/General",
"World Literature/China/19th Century",
"World Literature/China/20th Century",
"World Literature/China/21st Century",
"World Literature/Colombia",
"World Literature/Czech Republic",
"World Literature/Denmark",
"World Literature/England/General",
"World Literature/England/Early & Medieval Periods",
"World Literature/England/16th & 17th Century",
"World Literature/England/18th Century",
"World Literature/England/19th Century",
"World Literature/England/20th Century",
"World Literature/England/21st Century",
"World Literature/Europe (General)",
"World Literature/Finland",
"World Literature/France/General",
"World Literature/France/18th Century",
"World Literature/France/19th Century",
"World Literature/France/20th Century",
"World Literature/France/21st Century",
"World Literature/Germany/General",
"World Literature/Germany/20th Century",
"World Literature/Germany/21st Century",
"World Literature/Greece",
"World Literature/Hungary",
"World Literature/India/General",
"World Literature/India/19th Century",
"World Literature/India/20th Century",
"World Literature/India/21st Century",
"World Literature/Ireland/General",
"World Literature/Ireland/19th Century",
"World Literature/Ireland/20th Century",
"World Literature/Ireland/21st Century",
"World Literature/Italy",
"World Literature/Japan",
"World Literature/Korea",
"World Literature/Mexico",
"World Literature/Middle East/General",
"World Literature/Middle East/Arabian Peninsula",
"World Literature/Middle East/Egypt & North Africa",
"World Literature/Middle East/Israel",
"World Literature/Netherlands",
"World Literature/New Zealand",
"World Literature/Norway",
"World Literature/Oceania",
"World Literature/Pakistan",
"World Literature/Peru",
"World Literature/Poland",
"World Literature/Portugal",
"World Literature/Russia/General",
"World Literature/Russia/19th Century",
"World Literature/Russia/20th Century",
"World Literature/Russia/21st Century",
"World Literature/Scotland/General",
"World Literature/Scotland/19th Century",
"World Literature/Scotland/20th Century",
"World Literature/Scotland/21st Century",
"World Literature/South America (General)",
"World Literature/Southeast Asia",
"World Literature/Spain/General",
"World Literature/Spain/19th Century",
"World Literature/Spain/20th Century",
"World Literature/Spain/21st Century",
"World Literature/Sweden",
"World Literature/Turkey",
"World Literature/Uruguay",
"World Literature/Wales"
]

File diff suppressed because one or more lines are too long

View File

@@ -1975,6 +1975,7 @@ body {
.context-sp {background-color: var(--context_colors_soft_prompt);}
.context-genre {background-color: var(--context_colors_genre);}
.context-prompt {background-color: var(--context_colors_prompt);}
.context-wi {background-color: var(--context_colors_world_info);}
.context-memory {background-color: var(--context_colors_memory);}
@@ -2718,6 +2719,71 @@ body {
max-width: 99%;
}
/* Genres */
#genre-input {
height: 32px;
width: 100%;
}
#genre-suggestion-super-container { position: relative; }
#genre-suggestion-container {
position: absolute;
width: 100%;
left: 0px;
top: 0px;
background-color: var(--input_background);
z-index: 2;
max-height: calc((30px + 2px) * 5);
overflow-y: auto;
}
.genre-suggestion {
display: block;
text-align: center;
padding: 5px 0px;
background-color: var(--wi_tag_color);
margin-bottom: 2px;
}
.genre-suggestion.highlighted {
background-color: #151e28;
}
#genre-container {
max-height: 50vh;
overflow-y: auto;
background-color: #151e28;
}
.genre {
display: inline-flex;
align-items: center;
background-color: var(--wi_tag_color);
border-radius: var(--radius_wi_card);
padding: 4px 0px;
margin: 2px 2px;
}
.genre-inner {
display: flex;
justify-content: center;
position: relative;
width: 100%;
margin: 0px 10px;
top: 1px;
right: 2px;
}
.genre-inner > .x {
font-size: 16px;
cursor: pointer;
opacity: 0.7;
}
.genre-inner > .x:hover { opacity: 1; }
/*---------------------------------- Global ------------------------------------------------*/
.hidden {
display: none;

View File

@@ -70,12 +70,16 @@ var scroll_trigger_element = undefined; //undefined means not currently set. If
var drag_id = null;
const on_colab = $el("#on_colab").textContent == "true";
// Each entry into this array should be an object that looks like:
// {class: "class", key: "key", func: callback}
let sync_hooks = [];
// name, desc, icon, func
var finder_actions = [
{name: "Load Model", icon: "folder_open", type: "action", func: function() { socket.emit('load_model_button', {}); }},
{name: "New Story", icon: "description", type: "action", func: function() { socket.emit('new_story', ''); }},
{name: "Load Story", icon: "folder_open", type: "action", func: function() { socket.emit('load_story_list', ''); }},
{name: "Save Story", icon: "save", type: "action", func: function() { socket.emit("save_story", null, (response) => {save_as_story(response);}); }},
{name: "Load Story", icon: "folder_open", type: "action", func: load_story_list},
{name: "Save Story", icon: "save", type: "action", func: save_story},
{name: "Download Story", icon: "file_download", type: "action", func: function() { document.getElementById('download_iframe').src = 'json'; }},
{name: "Import Story", icon: "file_download", desc: "Import a prompt from aetherroom.club, formerly prompts.aidg.club", type: "action", func: openClubImport },
@@ -119,6 +123,7 @@ const context_menu_actions = {
"generated-image": [
{label: "View", icon: "search", enabledOn: "ALWAYS", click: imgGenView},
{label: "Download", icon: "download", enabledOn: "ALWAYS", click: imgGenDownload},
{label: "Retry", icon: "refresh", enabledOn: "ALWAYS", click: imgGenRetry},
{label: "Clear", icon: "clear", enabledOn: "ALWAYS", click: imgGenClear},
],
"wi-img-upload-button": [
@@ -129,8 +134,8 @@ const context_menu_actions = {
// CTRL-[X]
const shortcuts = [
{key: "k", desc: "Finder", func: open_finder},
{key: "/", desc: "Help screen", func: () => openPopup("shortcuts-popup")},
{key: "s", desc: "Save Story", func: save_story},
{key: "o", desc: "Open Story", func: load_story_list},
{key: "z", desc: "Undoes last story action", func: () => socket.emit("back", {}), criteria: canNavigateStoryHistory},
{key: "y", desc: "Redoes last story action", func: () => socket.emit("redo", {}), criteria: canNavigateStoryHistory},
{key: "e", desc: "Retries last story action", func: () => socket.emit("retry", {}), criteria: canNavigateStoryHistory},
@@ -138,6 +143,8 @@ const shortcuts = [
{key: "u", desc: "Focuses Author's Note", func: () => focusEl("#authors_notes")}, // CTRL-N is reserved :^(
{key: "g", desc: "Focuses game text", func: () => focusEl("#input_text")},
{key: "l", desc: '"Lock" screen (Not secure)', func: () => socket.emit("privacy_mode", {'enabled': true})},
{key: "k", desc: "Finder", func: open_finder},
{key: "/", desc: "Help screen", func: () => openPopup("shortcuts-popup")},
]
const chat = {
@@ -651,6 +658,9 @@ function do_probabilities(action) {
}
function save_story() { socket.emit("save_story", null, response => save_as_story(response)); }
function load_story_list() { socket.emit("load_story_list", ""); }
function do_presets(data) {
for (select of document.getElementsByClassName('presets')) {
//clear out the preset list
@@ -754,6 +764,12 @@ function var_changed(data) {
//if (data.name == "sp") {
// console.log({"name": data.name, "data": data});
//}
for (const entry of sync_hooks) {
if (data.classname !== entry.class) continue;
if (data.name !== entry.name) continue;
entry.func(data.value);
}
if (data.name in vars_sync_time) {
if (vars_sync_time[data.name] > Date.parse(data.transmit_time)) {
@@ -761,7 +777,7 @@ function var_changed(data) {
}
}
vars_sync_time[data.name] = Date.parse(data.transmit_time);
if ((data.classname == 'actions') && (data.name == 'Action Count')) {
current_action = data.value;
if (current_action <= 0) {
@@ -1008,7 +1024,7 @@ function load_popup(data) {
for (file of fileList) {
reader = new FileReader();
reader.onload = function (event) {
socket.emit("upload_file", {'filename': file.name, "data": event.target.result});
socket.emit("upload_file", {'filename': file.name, "data": event.target.result, 'upload_no_save': true});
};
reader.readAsArrayBuffer(file);
}
@@ -2612,80 +2628,86 @@ function create_new_softprompt() {
async function download_story_to_json() {
//document.getElementById('download_iframe').src = 'json';
downloaded = false;
async function download_story() {
if (socket.connected) {
try {
let r = await fetch("json");
let j = await r.json();
downloadString(JSON.stringify(j), j['story_name']+".json")
downloaded = true;
let name = $el(".var_sync_story_story_name").innerText;
let r = await fetch("story_download");
downloadBlob(await r.blob(), `${name}.kaistory`);
return;
}
catch(err) {
downloaded = false;
console.error("Error in online download");
console.error(err);
}
} if (downloaded == false) {
//first we're going to find all the var_sync_story_ classes used in the document.
let allClasses = [];
const allElements = document.querySelectorAll('*');
}
for (let i = 0; i < allElements.length; i++) {
let classes = allElements[i].classList;
for (let j = 0; j < classes.length; j++) {
if (!(allClasses.includes(classes[j].replace("var_sync_story_", ""))) && (classes[j].includes("var_sync_story_"))) {
allClasses.push(classes[j].replace("var_sync_story_", ""));
}
}
console.warn("Online download failed! Using offline download...")
/* Offline Download - Compile JSON file from what we have in ram */
//first we're going to find all the var_sync_story_ classes used in the document.
let allClasses = [];
const allElements = document.querySelectorAll('*');
for (let i = 0; i < allElements.length; i++) {
let classes = allElements[i].classList;
for (let j = 0; j < classes.length; j++) {
if (!(allClasses.includes(classes[j].replace("var_sync_story_", ""))) && (classes[j].includes("var_sync_story_"))) {
allClasses.push(classes[j].replace("var_sync_story_", ""));
}
//OK, now we're going to go through each of those classes and get the values from the elements
let j = {}
for (class_name of allClasses) {
for (item of document.getElementsByClassName("var_sync_story_"+class_name)) {
if (['INPUT', 'TEXTAREA', 'SELECT'].includes(item.tagName)) {
if ((item.tagName == 'INPUT') && (item.type == "checkbox")) {
j[class_name] = item.checked;
} else {
j[class_name] = item.value;
}
}
}
//OK, now we're going to go through each of those classes and get the values from the elements
let j = {}
for (class_name of allClasses) {
for (item of document.getElementsByClassName("var_sync_story_"+class_name)) {
if (['INPUT', 'TEXTAREA', 'SELECT'].includes(item.tagName)) {
if ((item.tagName == 'INPUT') && (item.type == "checkbox")) {
j[class_name] = item.checked;
} else {
j[class_name] = item.textContent;
j[class_name] = item.value;
}
break;
} else {
j[class_name] = item.textContent;
}
break;
}
//We'll add actions and world info data next
let temp = JSON.parse(JSON.stringify(actions_data));
delete temp[-1];
j['actions'] = {'action_count': document.getElementById('action_count').textContent, 'actions': temp};
j['worldinfo_v2'] = {'entries': world_info_data, 'folders': world_info_folder_data};
//Biases
let bias = {};
for (item of document.getElementsByClassName('bias')) {
let bias_phrase = item.querySelector(".bias_phrase").children[0].value;
let bias_score = parseInt(item.querySelector(".bias_score").querySelector(".bias_slider_cur").textContent);
let bias_comp_threshold = parseInt(item.querySelector(".bias_comp_threshold").querySelector(".bias_slider_cur").textContent);
if (bias_phrase != "") {
bias[bias_phrase] = [bias_score, bias_comp_threshold];
}
}
//We'll add actions and world info data next
let temp = JSON.parse(JSON.stringify(actions_data));
delete temp[-1];
j['actions'] = {'action_count': document.getElementById('action_count').textContent, 'actions': temp};
j['worldinfo_v2'] = {'entries': world_info_data, 'folders': world_info_folder_data};
//Biases
let bias = {};
for (item of document.getElementsByClassName('bias')) {
let bias_phrase = item.querySelector(".bias_phrase").children[0].value;
let bias_score = parseInt(item.querySelector(".bias_score").querySelector(".bias_slider_cur").textContent);
let bias_comp_threshold = parseInt(item.querySelector(".bias_comp_threshold").querySelector(".bias_slider_cur").textContent);
if (bias_phrase != "") {
bias[bias_phrase] = [bias_score, bias_comp_threshold];
}
j['biases'] = bias;
//substitutions
substitutions = [];
for (item of document.getElementsByClassName('substitution-card')) {
let target = item.children[0].querySelector(".target").value;
let sub = item.children[1].querySelector(".target").value;
let enabled = (item.children[1].querySelector(".material-icons-outlined").getAttribute("title") == 'Enabled');
substitutions.push({'target': target, 'substitution': sub, 'enabled': enabled});
}
j['substitutions'] = substitutions;
j['file_version'] = 2;
j['gamestarted'] = true;
downloadString(JSON.stringify(j), j['story_name']+".json")
}
}
j['biases'] = bias;
//substitutions
substitutions = [];
for (item of document.getElementsByClassName('substitution-card')) {
let target = item.children[0].querySelector(".target").value;
let sub = item.children[1].querySelector(".target").value;
let enabled = (item.children[1].querySelector(".material-icons-outlined").getAttribute("title") == 'Enabled');
substitutions.push({'target': target, 'substitution': sub, 'enabled': enabled});
}
j['substitutions'] = substitutions;
j['file_version'] = 2;
j['gamestarted'] = true;
downloadString(JSON.stringify(j), j['story_name']+".json")
}
function unload_userscripts() {
@@ -3197,6 +3219,7 @@ function autoResize(element, min_size=200) {
function calc_token_usage(
soft_prompt_length,
genre_length,
memory_length,
authors_note_length,
prompt_length,
@@ -3209,6 +3232,7 @@ function calc_token_usage(
const data = [
{id: "soft_prompt_tokens", tokenCount: soft_prompt_length, label: "Soft Prompt"},
{id: "genre_tokens", tokenCount: genre_length, label: "Genre"},
{id: "memory_tokens", tokenCount: memory_length, label: "Memory"},
{id: "authors_notes_tokens", tokenCount: authors_note_length, label: "Author's Note"},
{id: "world_info_tokens", tokenCount: world_info_length, label: "World Info"},
@@ -3525,6 +3549,7 @@ function update_context(data) {
$(".context-block").remove();
let memory_length = 0;
let genre_length = 0;
let authors_notes_length = 0;
let prompt_length = 0;
let game_text_length = 0;
@@ -3542,10 +3567,12 @@ function update_context(data) {
for (const entry of data) {
console.info(entry)
let contextClass = "context-" + ({
soft_prompt: "sp",
prompt: "prompt",
world_info: "wi",
genre: "genre",
memory: "memory",
authors_note: "an",
action: "action",
@@ -3592,6 +3619,9 @@ function update_context(data) {
document.getElementById('world_info_'+entry.uid).classList.add("used_in_game");
}
break;
case 'memory':
genre_length += entry.tokens.length;
break;
case 'memory':
memory_length += entry.tokens.length;
break;
@@ -3616,6 +3646,7 @@ function update_context(data) {
calc_token_usage(
soft_prompt_length,
genre_length,
memory_length,
authors_notes_length,
prompt_length,
@@ -4344,6 +4375,14 @@ function downloadString(string, fileName) {
a.click();
}
function downloadBlob(blob, fileName) {
const a = $e("a", null, {
href: URL.createObjectURL(blob),
download: fileName
});
a.click();
}
function getRedactedValue(value) {
if (typeof value === "string") return `[Redacted string with length ${value.length}]`;
if (value instanceof Array) return `[Redacted array with length ${value.length}]`;
@@ -4602,11 +4641,16 @@ async function loadNAILorebook(data, filename, image=null) {
}
}
async function loadKoboldData(data, filename) {
async function loadKoboldJSON(data, filename) {
if (data.gamestarted !== undefined) {
// Story
socket.emit("upload_file", {"filename": filename, "data": JSON.stringify(data)});
socket.emit("upload_file", {
filename: filename,
data: new Blob([JSON.stringify(data)]),
upload_no_save: true
});
socket.emit("load_story_list", "");
load_story_list();
} else if (data.folders !== undefined && data.entries !== undefined) {
// World Info Folder
await postWI(data);
@@ -4685,9 +4729,16 @@ async function processDroppedFile(file) {
readLoreCard(file);
break;
case "json":
// KoboldAI file
// KoboldAI file (old story, etc)
data = JSON.parse(await file.text());
loadKoboldData(data, file.name);
loadKoboldJSON(data, file.name);
break;
case "kaistory":
// KoboldAI story file
let r = await fetch(`/upload_kai_story/${file.name}`, {
method: "POST",
body: file
});
break;
case "lorebook":
// NovelAI lorebook, JSON encoded.
@@ -5467,6 +5518,8 @@ process_cookies();
return;
}
if (finder_mode !== "ui") return;
const actionsCount = actions.length;
let future = finder_selection_index + delta;
@@ -6426,4 +6479,152 @@ function imgGenClear() {
const container = $el("#action\\ image");
container.removeAttribute("tooltip");
socket.emit("clear_generated_image", {});
}
}
function imgGenRetry() {
const image = $el(".action_image");
if (!image) return;
$el("#image-loading").classList.remove("hidden");
socket.emit("retry_generated_image", {});
}
/* Genres */
(async function() {
const genreContainer = $el("#genre-container");
const genreInput = $el("#genre-input");
const genreSuggestionContainer = $el("#genre-suggestion-container");
let genreData = await (await fetch("/genre_data.json")).json();
let allGenres = genreData.list;
let genres = genreData.init;
let highlightIndex = -1;
sync_hooks.push({
class: "story",
name: "genres",
func: function(passedGenres) {
genres = passedGenres;
$(".genre").remove();
for (const g of genres) {
addGenreUI(g);
}
}
})
function addGenreUI(genre) {
let div = $e("div", genreContainer, {classes: ["genre"]});
let inner = $e("div", div, {classes: ["genre-inner"]});
let xIcon = $e("span", inner, {innerText: "clear", classes: ["x", "material-icons-outlined"]});
let label = $e("span", inner, {innerText: genre, classes: ["genre-label"]});
xIcon.addEventListener("click", function() {
div.remove();
genres = genres.filter(x => x !== genre);
socket.emit("var_change", {"ID": "story_genres", "value": genres});
});
}
for (const initGenre of genreData.init) {
addGenreUI(initGenre);
}
function addGenre(genre) {
if (genres.includes(genre)) return;
addGenreUI(genre);
genreInput.value = "";
nukeSuggestions();
genres.push(genre);
socket.emit("var_change", {"ID": "story_genres", "value": genres});
}
function nukeSuggestions() {
genreSuggestionContainer.innerHTML = "";
highlightIndex = -1;
}
document.addEventListener("click", function(event) {
// Listening for clicks all over the document kinda sucks but blur
// fires you can click a suggestion so...
if (!genreSuggestionContainer.children.length) return;
if (event.target === genreInput) return;
if (event.target.classList.contains("genre-suggestion")) return;
nukeSuggestions();
});
genreInput.addEventListener("keydown", function(event) {
switch (event.key) {
case "ArrowUp":
highlightIndex--;
break;
case "Tab":
highlightIndex += event.shiftKey ? -1 : 1;
break;
case "ArrowDown":
highlightIndex++;
break;
case "Enter":
if (highlightIndex === -1) {
if (!genreInput.value.trim()) return;
addGenre(genreInput.value);
} else {
genreSuggestionContainer.children[highlightIndex].click();
}
return;
case "Escape":
genreInput.value = "";
nukeSuggestions();
event.preventDefault();
event.stopPropagation();
return;
default:
return;
}
event.preventDefault();
if (!genreSuggestionContainer.children.length) return;
const oldHighlighted = $el(".genre-suggestion.highlighted");
if (oldHighlighted) oldHighlighted.classList.remove("highlighted");
// Wrap around
let maxIndex = genreSuggestionContainer.children.length - 1;
if (highlightIndex < 0) highlightIndex = maxIndex;
if (highlightIndex > maxIndex) highlightIndex = 0;
const highlighted = genreSuggestionContainer.children[highlightIndex];
highlighted.classList.add("highlighted");
highlighted.scrollIntoView({
behavior: "auto",
block: "center",
inline: "center"
});
});
genreInput.addEventListener("input", function() {
let showList = [];
let lowerMatch = genreInput.value.toLowerCase();
nukeSuggestions();
if (!lowerMatch) return;
for (const genre of allGenres) {
if (!genre.toLowerCase().includes(lowerMatch)) continue;
showList.push(genre);
}
for (const genre of showList) {
let suggestion = $e("span", genreSuggestionContainer, {
innerText: genre,
classes: ["genre-suggestion"]
});
suggestion.addEventListener("click", function() {
addGenre(this.innerText);
});
}
});
})()

View File

@@ -196,10 +196,11 @@
<span class="noselect">Key:</span>
<div>
<span class="noselect context-block-example context-sp">Soft Prompt</span>
<span class="noselect context-block-example context-prompt">Prompt</span>
<span class="noselect context-block-example context-wi">World Info</span>
<span class="noselect context-block-example context-genre">Genre</span>
<span class="noselect context-block-example context-memory">Memory</span>
<span class="noselect context-block-example context-wi">World Info</span>
<span class="noselect context-block-example context-an">Author's Note</span>
<span class="noselect context-block-example context-prompt">Prompt</span>
<span class="noselect context-block-example context-action">Action</span>
<span class="noselect context-block-example context-submit">Submit</span>
</div>

View File

@@ -60,7 +60,7 @@
<span class="var_sync_story_story_name fullwidth" contenteditable=true onblur="sync_to_server(this);"></span>
</span>
<span>
<span class="material-icons-outlined cursor" style="padding-top: 8px;" tooltip="Download Story" onclick="download_story_to_json()">file_download</span>
<span class="material-icons-outlined cursor" style="padding-top: 8px;" tooltip="Download Story" onclick="download_story();">file_download</span>
</span>
</div>
<div id="text_storyname">
@@ -71,11 +71,11 @@
<span class="material-icons-outlined cursor" tooltip="New Story">description</span>
<span class="button_label">New Story</span>
</button>
<button class="settings_button" onclick="socket.emit('load_story_list', '');">
<button class="settings_button" onclick="load_story_list();">
<span class="material-icons-outlined cursor" tooltip="Load Story">folder_open</span>
<span class="button_label">Load Story</span>
</button>
<button class="settings_button var_sync_alt_story_gamesaved" onclick='socket.emit("save_story", null, (response) => {save_as_story(response);});'>
<button class="settings_button var_sync_alt_story_gamesaved" onclick='save_story();'>
<span class="material-icons-outlined cursor var_sync_alt_story_gamesaved" tooltip="Save Story">save</span>
<span class="button_label">Save Story</span>
</button>

View File

@@ -50,6 +50,19 @@
<label for="authors_notes">Author's Notes:</label><br/>
<textarea autocomplete="off" rows=16 id="authors_notes" class="var_sync_story_authornote var_sync_alt_story_authornote_length fullwidth" oninput="autoResize(this)" onchange='sync_to_server(this);'></textarea><br/>
<h4 class="section_header">Genre</h4>
<div class="help_text">Styles the AI will attempt to imitate. Effectiveness depends on model.</div>
<input id="genre-input" class="fullwidth" autocomplete="off" spellcheck="false">
<div id="genre-suggestion-super-container">
<div id="genre-suggestion-container">
<span class="genre-suggestion">Animals</span>
<span class="genre-suggestion">Amish</span>
<span class="genre-suggestion">Asian</span>
<span class="genre-suggestion">Buddhist</span>
</div>
</div>
<div id="genre-container"></div>
<div id="An-Attention-Bias" class="var_sync_alt_system_experimental_features">
<h4 class="section_header"><label for="An-Attention-Bias">Attention Bias Test</label></h4>
<span class="help_text">See disclaimer in memory page.</span>
@@ -112,6 +125,7 @@
<div id="token-breakdown-container" class="settings_footer" style="padding-top: 10px;">
<div class="token_breakdown" onclick='socket.emit("update_tokens", document.getElementById("input_text").value);openPopup("context-viewer");'>
<div id="soft_prompt_tokens" style="width:0%; background-color: var(--context_colors_soft_prompt);"></div>
<div id="genre_tokens" style="width:40%; background-color: var(--context_colors_genre);"></div>
<div id="memory_tokens" style="width:40%; background-color: var(--context_colors_memory);"></div>
<div id="authors_notes_tokens" style="width:10%; background-color: var(--context_colors_authors_notes);"></div>
<div id="world_info_tokens" style="width:20%; background-color: var(--context_colors_world_info);"></div>

View File

@@ -151,6 +151,7 @@
--context_colors_submit: #ffffff00;
--context_colors_unused: #ffffff24;
--context_colors_soft_prompt: #141414;
--context_colors_genre: #2c5c88;
/*Parameters*/
--scrollbar-size: 6px;