diff --git a/backend/notepads.py b/backend/notepads.py index 882cee3..0bb61a6 100644 --- a/backend/notepads.py +++ b/backend/notepads.py @@ -21,6 +21,14 @@ from backend.util import MultiTimer notepad_list: dict or None = None current_notepad = None +# Cancel + +cancel_signal = False +def set_notepad_cancel_signal(): + global cancel_signal + cancel_signal = True + + def list_notepads(): global notepad_list @@ -84,8 +92,8 @@ def delete_notepad(d_notepad): def get_default_notepad_settings(): return \ { - "maxtokens": 1024, - "chunktokens": 512, + "maxtokens": 256, + "chunktokens": 64, "temperature": 0.8, "top_k": 50, "top_p": 0.8, @@ -97,7 +105,8 @@ def get_default_notepad_settings(): "typical": 0.0, "repp": 1.15, "repr": 1024, - "repd": 512 + "repd": 512, + "stop_conditions": [ { "text": "", "inclusive": False } ], } @@ -108,8 +117,11 @@ class Notepad: text = "" settings: {} = None + context_head = 0 + def __init__(self, notepad_uuid = None): self.notepad_uuid = notepad_uuid + self.context_head = 0 def filename(self): @@ -153,7 +165,7 @@ class Notepad: def save(self): - print(f"Saving notepad: {self.filename()}") + # print(f"Saving notepad: {self.filename()}") jd = json.dumps(self.to_json(), indent = 4) with open(self.filename(), "w") as outfile: outfile.write(jd) @@ -161,7 +173,7 @@ class Notepad: def load(self): - print(f"Loading notepad: {self.filename()}") + # print(f"Loading notepad: {self.filename()}") with open(self.filename(), "r") as s: j = json.load(s) self.from_json(j) @@ -188,4 +200,218 @@ class Notepad: t["id"] = token t["piece"] = m.tokenizer.extended_id_to_piece.get(token, id_to_piece[token]) tokenized.append(t) - return tokenized \ No newline at end of file + return tokenized + + + def get_gen_settings(self): + + gen_settings = ExLlamaV2Sampler.Settings() + gen_settings.temperature = self.settings["temperature"] + gen_settings.top_k = self.settings["top_k"] + gen_settings.top_p = self.settings["top_p"] + gen_settings.min_p = self.settings["min_p"] + gen_settings.tfs = self.settings["tfs"] + gen_settings.typical = self.settings["typical"] + gen_settings.mirostat = self.settings["mirostat"] + gen_settings.mirostat_tau = self.settings["mirostat_tau"] + gen_settings.mirostat_eta = self.settings["mirostat_eta"] + gen_settings.token_repetition_penalty = self.settings["repp"] + gen_settings.token_repetition_range = self.settings["repr"] + gen_settings.token_repetition_decay = self.settings["repr"] + + if gen_settings.temperature == 0: + gen_settings.temperature = 1.0 + gen_settings.top_k = 1 + gen_settings.top_p = 0 + gen_settings.typical = 0 + + return gen_settings + + + def generate_single_token(self, data): + + if get_loaded_model() is None: + packet = { "result": "fail", "error": "No model loaded." } + return packet + + model = get_loaded_model().model + generator = get_loaded_model().generator + tokenizer = get_loaded_model().tokenizer + cache = get_loaded_model().cache + + # Sampling settings + + gen_settings = self.get_gen_settings() + + # Context + + context_str = data["context"] + context_post_str = data["context_post"] + context_ids = tokenizer.encode(context_str, encode_special_tokens = True) + + # Truncate past + + head_ideal = context_ids.shape[-1] - model.config.max_seq_len + + chunk_size = self.settings["chunktokens"] + while head_ideal < self.context_head - chunk_size: + self.context_head -= chunk_size + if head_ideal > self.context_head: self.context_head = head_ideal + chunk_size - 1 + if self.context_head < 0: self.context_head = 0 + + context_ids = context_ids[:, self.context_head:] + # print(head_ideal, self.context_head) + + # Generate + + generator.begin_stream(context_ids, gen_settings, token_healing = True) + generator.set_stop_conditions([]) + + # Get one token + + chunk, eos, tokens = generator.stream() + t = tokens.item() + if t in tokenizer.extended_id_to_piece: chunk += tokenizer.extended_id_to_piece[t] + self.text = context_str + chunk + context_post_str + + # Save + + self.save() + + # Response + + packet = {} + packet["result"] = "ok" + packet["text"] = chunk + packet["tokenized_text"] = self.get_tokenized_text() + return packet + + + def generate(self, data): + global cancel_signal + + if get_loaded_model() is None: + packet = { "result": "fail", "error": "No model loaded." } + return packet + + model = get_loaded_model().model + generator = get_loaded_model().generator + tokenizer = get_loaded_model().tokenizer + cache = get_loaded_model().cache + + # Sampling settings + + gen_settings = self.get_gen_settings() + + # Context + + context_str = data["context"] + context_post_str = data["context_post"] + full_context_ids = tokenizer.encode(context_str, encode_special_tokens = True) + build_str = "" + + # Stop conditions + + exclusive_sc = [] + inclusive_sc = [] + for stop_condition in self.settings["stop_conditions"]: + text = stop_condition["text"].encode().decode('unicode_escape') + inclusive = stop_condition["inclusive"] + if inclusive: + inclusive_sc.append(text) + else: + if stop_condition["text"] in tokenizer.extended_piece_to_id: + exclusive_sc.append(tokenizer.extended_piece_to_id[text]) + else: + exclusive_sc.append(text) + + # Truncate past + + head_ideal = full_context_ids.shape[-1] - model.config.max_seq_len + chunk_size = self.settings["chunktokens"] + while head_ideal < self.context_head - chunk_size: + self.context_head -= chunk_size + + # Generator loop + + cancel_signal = False + + total_tokens = 0 + max_tokens = self.settings["maxtokens"] + prev_head = -1 + token_healing = True + while True: + + if cancel_signal: + break + + # Adjust context + + head_ideal = full_context_ids.shape[-1] - model.config.max_seq_len + if head_ideal > self.context_head: self.context_head = head_ideal + chunk_size - 1 + if self.context_head < 0: self.context_head = 0 + + # Begin stream + + if self.context_head != prev_head: + prev_head = self.context_head + context_ids = full_context_ids[:, self.context_head:] + generator.begin_stream(context_ids, gen_settings, token_healing = token_healing) + generator.set_stop_conditions(exclusive_sc) + token_healing = False + + # Get single token + + chunk, eos, tokens = generator.stream() + for i in range(tokens.shape[-1]): + t = tokens[0, i].item() + if t in tokenizer.extended_id_to_piece: chunk += tokenizer.extended_id_to_piece[t] + build_str += chunk + self.text = context_str + build_str + context_post_str + + # Stop conditions + + total_tokens += 1 + if total_tokens >= max_tokens: eos = True + else: + for s in inclusive_sc: + if s in build_str: + extra_chars = len(build_str) - (build_str.find(s) + len(s)) + if extra_chars > 0: + chunk = chunk[:-extra_chars] + build_str = build_str[:-extra_chars] + eos = True + break + + # Stream + + if chunk != "": + packet = {} + packet["result"] = "stream_chunk" + packet["text"] = chunk + yield json.dumps(packet) + "\n" + + if eos: break + + # Save + + self.save() + + # Response + + packet = {} + if cancel_signal: + packet["result"] = "cancel" + else: + packet["result"] = "ok" + packet["tokenized_text"] = self.get_tokenized_text() + yield json.dumps(packet) + "\n" + + packet = {} + packet["result"] = "ok" + return packet + + + + + diff --git a/backend/sessions.py b/backend/sessions.py index 72d5268..e68cdc4 100644 --- a/backend/sessions.py +++ b/backend/sessions.py @@ -162,14 +162,14 @@ class Session: def load(self): - print(f"Loading session: {self.filename()}") + # print(f"Loading session: {self.filename()}") with open(self.filename(), "r") as s: j = json.load(s) self.from_json(j) def save(self): - print(f"Saving session: {self.filename()}") + # print(f"Saving session: {self.filename()}") jd = json.dumps(self.to_json(), indent = 4) with open(self.filename(), "w") as outfile: outfile.write(jd) diff --git a/server.py b/server.py index 010ddb7..d22c4ee 100644 --- a/server.py +++ b/server.py @@ -11,7 +11,7 @@ import torch from backend.models import update_model, load_models, get_model_info, list_models, remove_model, load_model, unload_model, get_loaded_model from backend.config import set_config_dir, global_state from backend.sessions import list_sessions, set_session, get_session, get_default_session_settings, new_session, delete_session, set_cancel_signal -from backend.notepads import list_notepads, set_notepad, get_notepad, get_default_notepad_settings, new_notepad, delete_notepad +from backend.notepads import list_notepads, set_notepad, get_notepad, get_default_notepad_settings, new_notepad, delete_notepad, set_notepad_cancel_signal from backend.prompts import list_prompt_formats from backend.settings import get_settings, set_settings @@ -239,7 +239,7 @@ def api_generate(): def api_cancel_generate(): global api_lock_cancel, verbose if verbose: print("/api/cancel_generate") - with api_lock: + with api_lock_cancel: set_cancel_signal() result = { "result": "ok" } if verbose: print("->", result) @@ -362,7 +362,7 @@ def api_delete_notepad(): return json.dumps(result) + "\n" @app.route("/api/update_notepad_settings", methods=['POST']) -def update_notepad_settings(): +def api_update_notepad_settings(): global api_lock, verbose if verbose: print("/api/update_notepad_settings") with api_lock: @@ -375,7 +375,7 @@ def update_notepad_settings(): return json.dumps(result) + "\n" @app.route("/api/set_notepad_text", methods=['POST']) -def set_notepad_text(): +def api_set_notepad_text(): global api_lock, verbose if verbose: print("/api/set_notepad_text") with api_lock: @@ -388,6 +388,42 @@ def set_notepad_text(): if verbose: print("-> (...)") return json.dumps(result) + "\n" +@app.route("/api/notepad_single_token", methods=['POST']) +def api_notepad_single_token(): + global api_lock, verbose + if verbose: print("/api/notepad_single_token") + with api_lock: + n = get_notepad() + data = request.get_json() + if verbose: print("<-", data) + result = n.generate_single_token(data) + if verbose: print("-> (...)") + return json.dumps(result) + "\n" + + +@app.route("/api/notepad_generate", methods=['POST']) +def api_notepad_generate(): + global api_lock, verbose + if verbose: print("/api/notepad_generate") + with api_lock: + data = request.get_json() + if verbose: print("<-", data) + n = get_notepad() + if verbose: print("-> ..."); + result = Response(stream_with_context(n.generate(data)), mimetype = 'application/json') + if verbose: print("->", result) + return result + +@app.route("/api/cancel_notepad_generate") +def api_cancel_notepad_generate(): + global api_lock_cancel, verbose + if verbose: print("/api/cancel_notepad_generate") + with api_lock_cancel: + set_notepad_cancel_signal() + result = { "result": "ok" } + if verbose: print("->", result) + return result + # Prepare torch diff --git a/static/chat.css b/static/chat.css index ae584c5..b171085 100644 --- a/static/chat.css +++ b/static/chat.css @@ -95,7 +95,9 @@ align-items: center; display: flex; width: 60px; + height: 25px; justify-content: center; + padding-top: 8px; } .session-input { @@ -329,6 +331,7 @@ min-width: 90px; align-items: center; display: flex; + height: 28px; } .sss-item-sep { diff --git a/static/controls.js b/static/controls.js index 2d9b3f1..f3cc770 100644 --- a/static/controls.js +++ b/static/controls.js @@ -142,6 +142,106 @@ export class LabelTextboxButton extends LabelTextbox { } } +export class CheckboxTextboxButton { + constructor(nameid, classNameCheckbox, textCheckbox, classNameTextbox, placeholder, data, arrayName, index, data_id_text, data_id_cb, validateFunc, updateFunc, buttonText, buttonFunc) { + + this.data = data; + this.data_id_text = data_id_text; + this.data_id_cb = data_id_cb; + this.arrayName = arrayName; + this.index = index; + this.element = util.newVFlex("vflex_line"); + + // Checkbox + + this.cb = util.newDiv(null, classNameCheckbox); + + this.chkb = document.createElement("input"); + this.chkb.type = "checkbox"; + this.chkb.id = "checkbox_" + control_serial + "_" + nameid; + control_serial++; + this.chkb.className = "checkbox"; + + this.chkb_label = document.createElement("label"); + this.chkb_label.htmlFor = this.chkb.id; + this.chkb_label.className = "checkbox-label"; + this.chkb_label.innerHTML = textCheckbox; + + this.cb.appendChild(this.chkb); + this.cb.appendChild(this.chkb_label); + this.element.appendChild(this.cb); + + this.chkb.addEventListener("change", () => { + this.data[this.arrayName][this.index][this.data_id_cb] = this.chkb.checked; + if (updateFunc) updateFunc(); + }); + + // Textbox + + this.tb = document.createElement("input"); + this.tb.className = classNameTextbox; + this.tb.type = "text"; + + this.tb.addEventListener("focus", () => { + this.textbox_initial = this.tb.value; + }); + + this.tb.addEventListener("focusout", () => { + if (this.textbox_initial != this.tb.value) { + if (!validateFunc || validateFunc(this.tb.value)) { + this.data[this.arrayName][this.index][this.data_id_text] = this.interpret(this.tb.value); + if (updateFunc) updateFunc(); + } else { + this.tb.value = this.textbox_initial; + } + } + }); + + this.tb.addEventListener("keydown", (event) => { + if (event.key === "Escape") { + this.tb.value = this.textbox_initial; + this.tb.blur(); + event.preventDefault(); + } + if (event.key === "Enter" && !event.shiftKey) { + this.tb.blur(); + } + }); + + this.element.appendChild(this.tb); + + // Button + + this.buttonFunc = buttonFunc; + + this.button = document.createElement("span"); + this.button.className = "linkbutton enabled"; + this.button.innerHTML = buttonText; + + this.button.addEventListener("click", () => { + if (this.buttonFunc) this.buttonFunc(); + }); + + this.element.appendChild(this.button); + + this.refresh(); + } + + interpret(value) { + return value; + } + + refresh() { + //console.log(this.data); + this.chkb.checked = this.data[this.arrayName][this.index][this.data_id_cb]; + + let v = this.data[this.arrayName][this.index][this.data_id_text]; + v = v ? v : null; + this.tb.value = v; + } +} + + export class LabelNumbox extends LabelTextbox { constructor(classNameLabel, textLabel, className, placeholder, data, data_id, min, max, decimals, updateFunc, cb_auto_id = null) { super(classNameLabel, textLabel, className, placeholder, data, data_id, null, updateFunc, cb_auto_id); @@ -259,22 +359,38 @@ export class LabelCheckbox { } export class Button { - constructor(text, clickFunc, extra_style = null) { - this.element = util.newDiv(null, "textbutton", text); + constructor(text, clickFunc, extra_style = null, extra_text = null) { + if (!extra_text) { + this.element = util.newDiv(null, "textbutton", text); + } else { + this.element = util.newDiv(null, "textbutton"); + let div1 = util.newDiv(null, null, text); + let div2 = util.newDiv(null, "sub", extra_text); + this.element.appendChild(div1); + this.element.appendChild(div2); + } + if (extra_style) this.element.classList.add(extra_style); this.enabled = true; this.clickFunc = clickFunc; this.hidden = false; - this.element.addEventListener("click", () => { + this.element.addEventListener("mousedown", (event) => { + event.preventDefault(); + }); + + this.element.addEventListener("click", (event) => { if (!this.enabled) return; - if (this.clickFunc) this.clickFunc(); + if (this.clickFunc) { + event.preventDefault(); + this.clickFunc(); + } }); } - setEnabled(enabled) { + setEnabled(enabled, delay = 0) { this.enabled = enabled; - this.refresh(); + this.refresh(delay); } setHidden(hidden) { @@ -282,13 +398,26 @@ export class Button { this.refresh(); } - refresh() { + setVisible(visible) { + this.setHidden(!visible); + } + + refresh(delay = 0) { if (this.enabled) { + if (this.disablerTimeout) { + clearTimeout(this.disablerTimeout); + this.disablerTimeout = null; + } this.element.classList.add("enabled"); this.element.classList.remove("disabled"); } else { - this.element.classList.remove("enabled"); - this.element.classList.add("disabled"); + if (!this.disablerTimeout) { + this.disablerTimeout = setTimeout(() => { + this.element.classList.remove("enabled"); + this.element.classList.add("disabled"); + this.disabledTimeout = null; + }, delay); + } } if (this.hidden) { this.element.classList.add("hidden"); diff --git a/static/notepad.css b/static/notepad.css index 60e716f..265523e 100644 --- a/static/notepad.css +++ b/static/notepad.css @@ -83,22 +83,22 @@ } .notepad-editor { - display: flex; - flex-direction: column; - overflow-y: auto; - padding: 20px; + padding: 10px; padding-bottom: 0px; width: calc(100vw - 725px); color: var(--textcolor-text); font-size: var(--font-size-notepad); font-family: var(--font-family-notepad); line-height: var(--line-height-notepad); - white-space: pre-line; height: 100%; + border: 0px; + margin-left: 20px; + background-color: var(--background-color-chat); + resize: none; + min-height: 40px; } .notepad-editor:focus { - filter: brightness(110%); outline: none; } @@ -116,8 +116,12 @@ height: 0px; } +.token-view:focus { + outline: none; +} + .token-view-inner { - width: calc(100vw - 725px); + width: calc(100vw - 730px); display: flex; padding: 10px; flex-wrap: wrap; @@ -191,4 +195,40 @@ padding: 0px; border: 0px; margin-right: 0px; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); +} + +.notepad-bar { + height: 60px; + min-height: 60px; + max-height: 60px; + display: flex; + justify-content: right; + padding-top: 5px; +} + +.notepad-generate-button { + margin-top: auto; + margin-right: 0px; + margin-bottom: 10px; + margin-left: 10px; + padding-left: 12px; + align-items: center; + display: flex; + flex-direction: column; + width: 85px; + justify-content: center; + user-drag: none; + user-select: none; + line-height: 15px; + color: var(--textcolor-text); +} + +.notepad-generate-button .sub { + font-size: var(--font-size-tiny); +} + +.notepad-generate-button:hover { + filter: brightness(var(--hover-brightness)); + cursor: pointer; } diff --git a/static/notepad.js b/static/notepad.js index 59b54f1..56eb4de 100644 --- a/static/notepad.js +++ b/static/notepad.js @@ -32,12 +32,13 @@ export class Notepad { this.currentView = null; } - onEnter(getResponse = false) { + onEnter(getResponse = false, post = null) { fetch("/api/list_notepads") .then(response => response.json()) .then(response => { globals.receiveGlobals(response); this.populateNotepadList(response, getResponse); + if (post) post(); }); } @@ -131,20 +132,40 @@ class NotepadView { this.element.appendChild(this.editorView); this.element.appendChild(this.settingsView); - this.editor = document.createElement("div"); + this.editor = document.createElement("textarea"); this.editor.className = "notepad-editor"; - this.editor.contentEditable = "true"; this.editor.spellcheck = false; - this.editor.useRichtext = true; + //this.editor.contentEditable = "true"; + //this.editor.useRichtext = true; this.editor.addEventListener("paste", (event) => { this.paste(event); }); this.editor.addEventListener("input", (event) => { this.input(event); }); this.editor.addEventListener("blur", (event) => { this.blur(event); }); + this.editor.addEventListener("keydown", (event) => { this.keydown(event); }); + + document.addEventListener("keydown", (event) => { this.keydownView(event); }); this.divider = util.newDiv("notepad-view-divider", "notepad-view-divider"); this.tokenView = util.newDiv("token-view", "token-view"); this.tokenViewInner = util.newDiv("token-view-inner", "token-view-inner"); + this.controlBar = util.newDiv("notepad-bar", "notepad-bar"); + + this.generateTokenButton = new controls.Button("⏵ Token", () => { this.generateToken() }, "notepad-generate-button", "ctrl + enter"); + this.generateButton = new controls.Button("⯮ Generate", () => { this.generate() }, "notepad-generate-button", "shift + enter"); + this.cancelButton = new controls.Button("⏹ Stop", () => { this.cancelGenerate() }, "notepad-generate-button", "escape"); + if (!notepadID || notepadID == "new" || !globals.g.loadedModelUUID) { + this.generateTokenButton.setEnabled(false); + this.generateButton.setEnabled(false); + this.cancelButton.setEnabled(false); + } + this.cancelButton.setEnabled(false); + this.cancelButton.setVisible(false); + + this.controlBar.appendChild(this.generateTokenButton.element); + this.controlBar.appendChild(this.generateButton.element); + this.controlBar.appendChild(this.cancelButton.element); this.editorView.appendChild(this.editor); + this.editorView.appendChild(this.controlBar); this.editorView.appendChild(this.divider); this.editorView.appendChild(this.tokenView); this.tokenView.appendChild(this.tokenViewInner); @@ -154,11 +175,11 @@ class NotepadView { document.addEventListener("mouseup", (event) => { this.isResizing = false; }); document.addEventListener("mousemove", (event) => { if (!this.isResizing) return; - let r = this.editor.getBoundingClientRect(); + //let r = this.editor.getBoundingClientRect(); let r2 = this.element.getBoundingClientRect(); - let newHeight = event.clientY - r.top - 35; - let newHeight2 = r2.height - newHeight - 70; - if (newHeight2 < 5) newHeight2 = 0; + let newHeight = event.clientY - 95; + let newHeight2 = r2.height - newHeight - 165; + //if (newHeight2 < 5) newHeight2 = 0; this.editor.style.height = "" + newHeight + "px"; this.tokenView.style.height = "" + newHeight2 + "px"; }); @@ -240,17 +261,19 @@ class NotepadView { } setText(text) { - this.editor.innerHTML = ""; - let lines = text.split('\n'); - for (let i = 0; i < lines.length; i++) { - let div = document.createElement("div"); - let t = lines[i]; - if (t.startsWith(" ")) t = "\xa0" + t.slice(1); - t = t.replace(/( +) /g, function(match, p1) { return p1.replace(/ /g, "\xa0") + " "; }); - div.innerText = t; - if (lines[i] == "") div.innerHTML += "
"; - this.editor.appendChild(div); - } + this.editor.value = text; + this.restoreCursor(); +// this.editor.innerHTML = ""; +// let lines = text.split('\n'); +// for (let i = 0; i < lines.length; i++) { +// let div = document.createElement("div"); +// let t = lines[i]; +// if (t.startsWith(" ")) t = "\xa0" + t.slice(1); +// t = t.replace(/( +) /g, function(match, p1) { return p1.replace(/ /g, "\xa0") + " "; }); +// div.innerText = t; +// if (lines[i] == "") div.innerHTML += "
"; +// this.editor.appendChild(div); +// } } getTextFrom(element) { @@ -270,19 +293,37 @@ class NotepadView { } getText() { - let t = this.getTextFrom(this.editor).slice(0, -1); - return t; + //let t = this.getTextFrom(this.editor).slice(0, -1); + //return t; + return this.editor.value; + } + + saveCursor() { + this.parent.selectionStart = this.editor.selectionStart; + this.parent.selectionEnd = this.editor.selectionEnd; + //console.log("save"); + } + + restoreCursor() { + if (this.parent.selectionStart) { + console.log("restore"); + this.editor.setSelectionRange(this.parent.selectionStart, this.parent.selectionEnd); + this.editor.focus(); + this.parent.selectionStart = null; + this.parent.selectionEnd = null; + } } input(event) { if (!this.notepadID || this.notepadID == "new") { + this.saveCursor(); let packet = {}; - packet.text = this.editor.innerText; + packet.text = this.getText(); fetch("/api/new_notepad", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(packet) }) .then(response => response.json()) .then(response => { this.parent.lastNotepadUUID = response.notepad.notepad_uuid; - this.parent.onEnter(); + this.parent.onEnter(false); }); return; } @@ -310,7 +351,7 @@ class NotepadView { fetch("/api/set_notepad_text", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(packet) }) .then(response => response.json()) .then(response => { - console.log(response); + //console.log(response); if (response.tokenized_text) this.updateTokens(response.tokenized_text); }); } @@ -351,6 +392,199 @@ class NotepadView { this.tokenView.style.height = prevHeight; } } + + keydown(event) { + if (event.key === "Enter" && event.shiftKey) { + event.preventDefault(); + if (this.generateButton.enabled) + this.generate(); + } + if (event.key === "Enter" && event.ctrlKey) { + event.preventDefault(); + if (this.generateTokenButton.enabled) + this.generateToken(); + } + } + + keydownView(event) { + if (event.key === "Escape") { + if (this.cancelButton.enabled) { + this.cancelGenerate(); + } + } + } + + generateToken() { + if (!globals.g.loadedModelUUID) return; + + this.generateButton.setEnabled(false, 200); + this.generateTokenButton.setEnabled(false, 200); + this.editor.disabled = true; + + let pos = this.editor.selectionStart; + let end = this.editor.selectionEnd; + let text = this.editor.value; + + if (end > pos) { + this.editor.value = this.editor.value.slice(0, pos) + this.editor.value.slice(end); + } + + let packet = {}; + packet.position = pos; + packet.context = this.editor.value.slice(0, pos); + packet.context_post = this.editor.value.slice(pos); + + fetch("/api/notepad_single_token", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(packet) }) + .then(response => response.json()) + .then(response => { + if (response.text) + { +// console.log(pos, response.text.length); + this.editor.value = packet.context + response.text + packet.context_post; + this.editor.setSelectionRange(pos + response.text.length, pos + response.text.length); + this.updateTokens(response.tokenized_text); + this.editor.disabled = false; + this.editor.focus(); + } + this.generateButton.setEnabled(true); + this.generateTokenButton.setEnabled(true); + this.editor.disabled = false; + }); + } + + generate() { + if (!globals.g.loadedModelUUID) return; + + this.generateButton.setEnabled(false); + this.generateButton.setVisible(false); + this.cancelButton.setEnabled(true); + this.cancelButton.setVisible(true); + this.generateTokenButton.setEnabled(false); + this.editor.disabled = true; + + let pos = this.editor.selectionStart; + let end = this.editor.selectionEnd; + let text = this.editor.value; + + if (end > pos) { + this.editor.value = this.editor.value.slice(0, pos) + this.editor.value.slice(end); + } + + this.receiveStreamPos = pos; + + let packet = {}; + packet.position = pos; + packet.context = this.editor.value.slice(0, pos); + packet.context_post = this.editor.value.slice(pos); + + let timeout = new Promise((resolve, reject) => { + let id = setTimeout(() => { + clearTimeout(id); + reject('No response from server') + }, 10000) + }); + + let fetchRequest = fetch("/api/notepad_generate", { + method: "POST", + headers: { "Content-Type": "application/json", }, + body: JSON.stringify(packet) + }); + + const self = this; + Promise.race([fetchRequest, timeout]) + .then(response => { + if (response.ok) { + return response.body; + } else { + throw new Error("Network response was not ok."); + } + }) + .then(stream => { + let reader = stream.getReader(); + let decoder = new TextDecoder(); + let data = ''; + reader.read().then(function process({done, value}) { + // console.log("Received chunk:", decoder.decode(value)); + if (done) { + //console.log("DONE"); + self.generateButton.setEnabled(true); + self.generateButton.setVisible(true); + self.generateTokenButton.setEnabled(true); + self.cancelButton.setEnabled(false); + self.cancelButton.setVisible(false); + self.editor.disabled = false; + self.editor.focus(); + return; + } + data += decoder.decode(value, {stream: true}); + let lines = data.split('\n'); + for (let i = 0; i < lines.length - 1; i++) { + let json = null; + try { + json = JSON.parse(lines[i]); + } catch(e) { + console.error("Invalid JSON:", lines[i]); + break; + } + if (json.result == "fail") { + console.error('Error:', json.error); + self.generateButton.setEnabled(true); + self.generateButton.setVisible(true); + self.generateTokenButton.setEnabled(true); + self.cancelButton.setEnabled(false); + self.cancelButton.setVisible(false); + self.editor.disabled = false; + self.editor.focus(); + return; + } else { + self.receivedStreamResponse(json); + } + } + data = lines[lines.length - 1]; + reader.read().then(process); + }); + }) + .catch(error => { + console.error('Error:', error); + self.enableInput(); + self.focusInputField(); + }) + } + + receivedStreamResponse(response) { + //console.log(response); + + if (response.result == "stream_chunk") { + let chunk = response.text; + let pos = this.receiveStreamPos; + this.editor.value = this.editor.value.slice(0, pos) + chunk + this.editor.value.slice(pos); + pos += chunk.length; + this.editor.setSelectionRange(pos, pos); + this.receiveStreamPos = pos; + } + + if (response.result == "ok") { + if (response.tokenized_text) { + this.updateTokens(response.tokenized_text); + } + } + + if (response.result == "cancel") { + if (response.tokenized_text) { + this.updateTokens(response.tokenized_text); + } + } + } + + cancelGenerate() { + this.cancelButton.setEnabled(false); + //console.log("cancel"); + fetch("/api/cancel_notepad_generate") + .then(response => response.json()) + .then(response => { + }); + } + } diff --git a/static/notepadsettings.js b/static/notepadsettings.js index b7e9826..5accd6d 100644 --- a/static/notepadsettings.js +++ b/static/notepadsettings.js @@ -26,8 +26,10 @@ export class NotepadSettings { this.sss_i_maxTokens = new controls.SettingsSlider("sss-item-left", "Max tokens", "sss-item-mid", "sss-item-right sss-item-textbox-r", 0, 16, 2048, null, this.settings, "maxtokens", () => { this.updateView(true); }); this.sss_i_chunkTokens = new controls.SettingsSlider("sss-item-left", "Chunk tokens", "sss-item-mid", "sss-item-right sss-item-textbox-r", 0, 16, 2048, null, this.settings, "chunktokens", () => { this.updateView(true); }); + this.sss_stopConditions = new controls.CollapsibleSection(null, "Stop conditions"); this.sss_genParams.inner.appendChild(this.sss_i_maxTokens.element); this.sss_genParams.inner.appendChild(this.sss_i_chunkTokens.element); + this.element.appendChild(this.sss_stopConditions.element); // Sampling @@ -57,11 +59,53 @@ export class NotepadSettings { this.sss_sampling.inner.appendChild(this.sss_i_mirostat_tau.element); this.sss_sampling.inner.appendChild(this.sss_i_mirostat_eta.element); - // . + // Stop conditions + + this.populate_stop_conditions(); this.updateView(); } + populate_stop_conditions() { + this.sss_stopConditions.inner.innerHTML = ""; + this.sss_i_stopconditions = []; + + for (let i = 0; i < this.settings.stop_conditions.length; i++) { + this.sss_i_stopconditions[i] = new controls.CheckboxTextboxButton( + "stop_condition_" + i, + "sss-item-left", + "Incl.", + "sss-item-mid sss-item-textbox", + "", + this.settings, + "stop_conditions", + i, + "text", + "inclusive", + (v) => { return v != ""; }, + () => { this.updateView(true); }, + "✕ Remove", + () => { + this.settings.stop_conditions.splice(i, 1); + this.populate_stop_conditions(); + this.updateView(true); + } + ); + } + + for (let i = 0; i < this.settings.stop_conditions.length; i++) + this.sss_stopConditions.inner.appendChild(this.sss_i_stopconditions[i].element); + + if (this.settings.stop_conditions.length < 10) { + this.sss_i_addStopCondition = new controls.LinkButton("+ Add...", null, () => { + this.settings.stop_conditions.push({text: "", inclusive: false}); + this.populate_stop_conditions(); + this.updateView(true); + }, "sss-item-link"); + this.sss_stopConditions.inner.appendChild(this.sss_i_addStopCondition.element); + } + } + updateView(send = false) { // Settings visibility @@ -77,7 +121,7 @@ export class NotepadSettings { } send(post = null) { - console.log(this.settings); + //console.log(this.settings); let packet = {}; packet.settings = this.settings; diff --git a/static/theme.css b/static/theme.css index 10bf677..9ec5466 100644 --- a/static/theme.css +++ b/static/theme.css @@ -6,7 +6,7 @@ --font-size-chat: 16px; --font-size-medium: 14px; --font-size-small: 12px; - --font-size-tiny: 9px; + --font-size-tiny: 10px; --font-family-notepad: Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace; --font-size-notepad: 16px; @@ -130,8 +130,12 @@ --background-color-code: rgba(23, 26, 28, 255); --border-color-code: rgba(44, 47, 52, 255); --textcolor-code: rgba(200, 217, 208, 255); + + --checkbox-accent-color: rgba(84, 90, 118, 255); + --checkbox-accent-border: rgba(84, 90, 118, 255); } + /* Light */ .theme-light {