const noop = () => {}; createUIElements(); let currentModelUUID = null; let loadedModelUUID = null; let failedModelUUID = null; let failedModelError = null; let currentSessionUUID = null; let currentSettings = null; let currentStreamingBlock = null; let keyboard_disabled = false; document.addEventListener("keydown", function (e) { if (keyboard_disabled) e.preventDefault(); }); document.addEventListener('DOMContentLoaded', () => { const textarea = document.getElementById('session-input'); textarea.addEventListener('input', autoGrow); textarea.addEventListener('keydown', function(event) { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); submitInput(); } }); function autoGrow() { textarea.style.height = 'auto'; textarea.style.height = (textarea.scrollHeight) + 'px'; } }); let textbox_initial = ""; let roleAvatars = [ "/static/gfx/avatar_frog.png", "/static/gfx/avatar_cat.png", "/static/gfx/avatar_dog.png", "/static/gfx/avatar_monke.png", "/static/gfx/avatar_unicorn.png", "/static/gfx/avatar_squirrel.png", "/static/gfx/avatar_penguin.png", "/static/gfx/avatar_notcat.png" ]; let roleColors = [ "rgba(167, 231, 0, 255)", "rgba(136, 70, 104, 255)", "rgba(58, 90, 208, 255)", "rgba(215, 86, 98, 255)", "rgba(220, 170, 62, 255)", "rgba(40, 170, 170, 255)", "rgba(170, 170, 170, 255)", "rgba(120, 120, 120, 255)" ]; let fallbackAvatar = "/static/gfx/avatar_notcat.png"; let fallbackColor = "rgba(120, 120, 120, 255)"; let instructAvatars = [ "/static/gfx/avatar_frog.png", "/static/gfx/avatar_cat.png", ]; let instructColors = [ "rgba(167, 231, 0, 255)", "rgba(136, 70, 104, 255)", ]; let statsvisible = false; function newDiv(className = null, id = null, html = null, parent = null, editFunc = null) { let nd = document.createElement('div'); if (className) nd.className = className; if (id) nd.id = id; if (html) nd.innerHTML = html; if (parent) parent.appendChild(nd); if (editFunc) makeDivEditable(nd, editFunc); return nd; } function makeDivEditable(nd, editFunc) { nd.classList.add("editable"); nd.addEventListener('click', function() { makeEditable(nd, editFunc) }); } function getTextWithLineBreaks(node) { let text = ""; for (let child of node.childNodes) { if (child.nodeType === Node.TEXT_NODE) { text += child.textContent.trim(); } else if (child.tagName === "BR") { text += "\n"; } } return text; } function addTextboxEvents(ctb, onValueChange, multiline = false, onExit = null) { ctb.addEventListener("focus", function() { textbox_initial = this.value; }); ctb.addEventListener("focusout", function() { if (textbox_initial !== this.value) { //console.log(this.id, this.value); onValueChange(this.id, this.value); } if (onExit) { onExit(this.id); } }); ctb.addEventListener("keydown", function(event) { if (event.key === "Escape") { this.value = textbox_initial; this.blur(); event.preventDefault(); } if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); this.blur(); } if (event.key == "Enter" && !multiline) { this.blur(); event.preventDefault(); } }); } function makeEditable(div, onValueChange, onExit, in_text = null, multiline = false) { let dstyle = window.getComputedStyle(div); let text = in_text; if (!in_text) { text = getTextWithLineBreaks(div); } let rect = div.getBoundingClientRect(); let computedStyle = window.getComputedStyle(div); let textarea = document.createElement("textarea"); textbox_initial = text; addTextboxEvents(textarea, onValueChange, multiline, onExit); textarea.style.top = `${rect.top}px`; textarea.style.left = `${rect.left}px`; textarea.style.width = `${rect.width}px`; textarea.style.height = `${rect.height}px`; textarea.style.border = "0px"; textarea.style.outline = computedStyle.outline; textarea.style.padding = computedStyle.padding; textarea.style.margin = computedStyle.margin; textarea.style.resize = computedStyle.resize; textarea.style.fontFamily = computedStyle.fontFamily; textarea.style.fontSize = computedStyle.fontSize; textarea.style.fontWeight = computedStyle.fontWeight; textarea.style.color = computedStyle.color; textarea.style.backgroundColor = computedStyle.backgroundColor; textarea.style.borderRadius = computedStyle.borderRadius; textarea.style.boxSizing = 'border-box'; textarea.style.verticalAlign = computedStyle.verticalAlign; textarea.style.lineHeight = computedStyle.lineHeight; textarea.style.textAlign = computedStyle.textAlign; textarea.style.padding = computedStyle.padding; textarea.style.minHeight = `${rect.height}px`; textarea.rows = 1; textarea.value = text; textarea.addEventListener('blur', function () { div.innerHTML = textarea.value; div.style.display = ""; textarea.parentNode.removeChild(textarea); }); div.parentNode.insertBefore(textarea, div.nextSibling); div.style.display = "none"; textarea.focus(); } function newButton(html = null, parent = null, style = null, clickFunc = null, enabled = true) { let nd = newDiv("textbutton", null, html, parent); if (style) nd.classList.add(style); if (enabled) nd.classList.add("enabled"); else nd.classList.add("disabled"); nd.addEventListener('click', clickFunc); return nd; } function createMainMenuButton(id, text, graphic, tabName, enterFunc) { let menu = document.getElementById('mainmenu'); let html = ""; html += "

" + text + "

"; var nd = newDiv("mainmenu-button inactive", id, html, menu); nd.dataset.tabName = tabName; document.getElementById(tabName).classList.add('hidden'); nd.addEventListener('click', function() { let menu = document.getElementById('mainmenu'); for (let i = 0; i < menu.children.length; i++) { let c = menu.children[i]; if (c.nodeName === 'DIV') { var page = c.dataset.tabName; if (c == nd) { c.classList.add("active"); c.classList.remove("inactive"); document.getElementById(page).classList.remove('hidden'); } else { c.classList.add("inactive"); c.classList.remove("active"); document.getElementById(page).classList.add('hidden'); } } } enterFunc(); }); return nd; } function addIcon(div, icon_id, size = 24) { var ns = "http://www.w3.org/2000/svg"; var svg = document.createElementNS(ns, "svg"); svg.setAttributeNS(null, "width", size); svg.setAttributeNS(null, "height", size); svg.style.top = "8px"; var use = document.createElementNS(ns, "use"); use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', "#" + icon_id); svg.appendChild(use); div.style.position = "relative"; div.appendChild(svg); return svg; } function addModel(name, modelID) { let model_list = document.getElementById('model-list'); var nd = newDiv("model-list-entry inactive", "model_" + modelID, null, model_list); if (modelID == "new") { addIcon(nd, "model-plus-icon"); } else if (modelID == loadedModelUUID) { let svg = addIcon(nd, "model-loaded-icon"); svg.classList.add("active"); } else { addIcon(nd, "model-icon"); } var ndi = newDiv(null, null, "

" + name + "

", nd); nd.dataset.modelID = modelID; nd.addEventListener('click', function() { model_list = document.getElementById('model-list'); for (let i = 0; i < model_list.children.length; i++) { let c = model_list.children[i]; if (c.nodeName === 'DIV') { if (c == nd) { c.classList.add("active"); c.classList.remove("inactive"); } else { c.classList.add("inactive"); c.classList.remove("active"); } } } showModel(nd.dataset.modelID); }); } function populateModelList(model_names) { model_list = document.getElementById('model-list'); model_list.innerHTML = ""; for (let model_uuid in model_names) { if (model_names.hasOwnProperty(model_uuid)) { // console.log("----"); // console.log(model_uuid, model_names[model_uuid]); addModel(model_names[model_uuid], model_uuid); } } addModel("New model", "new"); let def = document.getElementById(currentModelUUID ? "model_" + currentModelUUID : "model_new"); //console.log(def); //console.log(currentModelUUID); def.click(); } function enterModelPage() { fetch("/api/list_models") .then(response => response.json()) .then(json => { //console.log(json); if (json.current_model) loadedModelUUID = json.current_model; if (!currentModelUUID && json.current_model) currentModelUUID = json.current_model; populateModelList(json.models); }); } function addModelItem(left, right, style = null) { let model_view = document.getElementById('model-view'); var nd = newDiv("model-view-item", null, null, model_view); var ndl = newDiv("model-view-item-left", null, left, nd); var ndr = newDiv("model-view-item-right" + (style ? (" " + style) : ""), null, right, nd); return ndr; } function addModelItem_textbox(left, id = null, text = null, editFunc = null, default_text = null, extra_style = null, append = null, prepend = null) { let model_view = document.getElementById('model-view'); var ndr; if (!append && !prepend) ndr = addModelItem(left, null); else ndr = append ? append : prepend; var tb = document.createElement('input'); tb.className = "textbox"; tb.type = 'text'; if (text) tb.value = text; if (id) tb.id = id; if (default_text) tb.placeholder = default_text; if (extra_style) tb.classList.add(extra_style); if (editFunc) { tb.addEventListener("focus", function() { textbox_initial = this.value; }); tb.addEventListener("focusout", function() { if (textbox_initial !== this.value) { editFunc(); } }); tb.addEventListener("keydown", function(event) { if (event.key === "Escape") { this.value = textbox_initial; this.blur(); event.preventDefault(); } if (event.key === "Enter" && !event.shiftKey) { this.blur(); } // if (event.key == "Enter" && !multiline) // { // this.blur(); // event.preventDefault(); // } }); } if (prepend) ndr.prepend(tb); else ndr.appendChild(tb); return ndr; } function addModelItem_checkbox(left, id = null, text = null, state = null, editFunc = null, extra_style = null) { let model_view = document.getElementById('model-view'); //console.log("state", state); var ndr = addModelItem(left, null); var ndri = document.createElement('div'); ndri.className = "checkbox"; if (extra_style) ndri.classList.add(extra_style); var cb = document.createElement('input'); cb.type = 'checkbox'; //cb.className = "checkbox"; cb.checked = state; if (id) cb.id = id; var label = document.createElement('label'); if (id) label.htmlFor = id; //label.className = "checkbox" label.appendChild(document.createTextNode(text)); if (editFunc) cb.addEventListener("change", function(event) { editFunc() } ); ndri.appendChild(cb); ndri.appendChild(label); ndr.appendChild(ndri); return ndr; } function addModelItem_combo(left, id = null, options = null, selected = null, editFunc = null, extra_style = null ) { let model_view = document.getElementById('model-view'); var ndr = addModelItem(left, null); var cb = document.createElement('select'); cb.className = "combobox"; if (id) cb.id = id; if (extra_style) cb.classList.add(extra_style); for (let i = 0; i < options.length; i++) cb.add(new Option(options[i], options[i])); cb.value = selected; if (editFunc) cb.addEventListener("change", function(event) { editFunc() } ); ndr.appendChild(cb); return ndr; } function showModel_(model_info) { //console.log(model_info); let focus = saveFocus(); let model_view = document.getElementById('model-view'); model_view.innerHTML = ""; // Name var model_name = model_info.name; var nd = newDiv("vflex", null, "", model_view); newDiv("model-view-text heading", "div-model-name", model_name, nd, sendCurrentModelView); if (model_info.model_uuid != "new") newButton("✖ Remove", nd, "small", removeCurrentModel); let def = document.getElementById("model_" + model_info.model_uuid); def.children[1].innerHTML = "

" + model_name + "

"; newDiv("model-view-text divider", null, "", model_view); newDiv("model-view-text spacer", null, "", model_view); // Directory var model_dir = model_info["model_directory"] || null; nd = addModelItem_textbox("Model path", "tb-model-view-directory", model_dir, sendCurrentModelView, "~/models/my_model/", "wide"); if (!model_dir || model_dir == "") return; newDiv("model-view-text spacer", null, "", model_view); if (model_info["config_status"] != "ok") { nd = addModelItem("", model_info["config_status_error"], "error"); return; } // Stats var stats = model_info["stats"]; addModelItem("Layers", stats["num_hidden_layers"]); addModelItem("Dimension", stats["hidden_size"] + " (" + stats["intermediate_size"] + ")"); var heads_q = stats["num_attention_heads"]; var heads_kv = stats["num_key_value_heads"]; var head_dim = stats["head_dim"]; if (heads_q == heads_kv) { addModelItem("Heads", heads_q + ", dim: " + head_dim); } else { addModelItem("Heads", "Q: " + heads_q + ", K/V: " + heads_kv + " (GQA), dim: " + head_dim); } addModelItem("Vocab size", stats["vocab_size"]); // Context // newDiv("model-view-text spacer", null, "", model_view); // newDiv("model-view-text divider", null, "", model_view); newDiv("model-view-text spacer", null, "", model_view); nd = addModelItem_textbox("Context length", "tb-model-seq-len", "" + model_info.seq_len, sendCurrentModelView, null, "shortright"); newDiv("model-view-text extra", null, "(native: " + model_info.default_seq_len + ")", nd); addModelItem_textbox("RoPE scale", "tb-model-rope-scale", model_info.rope_scale.toFixed(2), sendCurrentModelView, null, "shortright"); addModelItem_textbox("RoPE alpha", "tb-model-rope-alpha", model_info.rope_alpha.toFixed(2), sendCurrentModelView, null, "shortright"); addModelItem_combo("Cache mode", "cb-model-cache-mode", [ "FP16", "FP8" ], model_info.cache_mode, sendCurrentModelView, "short"); addModelItem_textbox("Chunk size", "tb-model-chunk-size", "" + model_info.chunk_size, sendCurrentModelView, null, "shortright"); nd = addModelItem_checkbox("GPU split", "cb-model-gpu-split-auto", "Auto", model_info.gpu_split_auto, sendCurrentModelView, "short"); if (!model_info.gpu_split_auto) addModelItem_textbox("GPU split", "tb-model-gpu-split", "" + model_info.gpu_split, sendCurrentModelView, null, "shortright", null, nd); // Draft model newDiv("model-view-text spacer", null, "", model_view); newDiv("model-view-text divider", null, "", model_view); newDiv("model-view-text spacer", null, "", model_view); addModelItem_checkbox("Speculative decoding", "cb-model-spec-dec", "Enabled", model_info.draft_enabled, sendCurrentModelView, "short"); if (model_info.draft_enabled) { var draft_model_dir = model_info["draft_model_directory"] || null; addModelItem_textbox("Draft model path", "tb-model-view-draft-directory", draft_model_dir, sendCurrentModelView, "~/models/my_model/", "wide"); if (!draft_model_dir || draft_model_dir == "") return; if (model_info["draft_config_status"] != "ok") { nd = addModelItem("", model_info["draft_config_status_error"], "error"); return; } newDiv("model-view-text spacer", null, "", model_view); // Draft model stats var stats = model_info["draft_stats"]; addModelItem("Layers", stats["num_hidden_layers"]); addModelItem("Dimension", stats["hidden_size"] + " (" + stats["intermediate_size"] + ")"); var heads_q = stats["num_attention_heads"]; var heads_kv = stats["num_key_value_heads"]; var head_dim = stats["head_dim"]; if (heads_q == heads_kv) { addModelItem("Heads", heads_q + ", dim: " + head_dim); } else { addModelItem("Heads", "Q: " + heads_q + ", K/V: " + heads_kv + " (GQA), dim: " + head_dim); } addModelItem("Vocab size", stats["vocab_size"]); // Draft options newDiv("model-view-text spacer", null, "", model_view); if (draft_model_dir && draft_model_dir != "") { nd = addModelItem_checkbox("RoPE alpha", "cb-model-draft-rope-alpha-auto", "Auto", model_info.draft_rope_alpha_auto, sendCurrentModelView, "short"); if (!model_info.draft_rope_alpha_auto) addModelItem_textbox("RoPE alpha", "tb-model-draft-rope-alpha", (model_info.draft_rope_alpha || 1.0).toFixed(2), sendCurrentModelView, null, "shortright", null, nd); } } // Load newDiv("model-view-text spacer", null, "", model_view); newDiv("model-view-text divider", null, "", model_view); newDiv("model-view-text spacer", null, "", model_view); newDiv("model-view-text spacer", null, "", model_view); nd = newDiv("vflex", null, "", model_view); button_load = newButton("⏵ Load model", nd, null, loadCurrentModel, (model_info.model_uuid != loadedModelUUID)); button_unload = newButton("⏹ Unload model", nd, null, unloadCurrentModel, (model_info.model_uuid == loadedModelUUID)); newDiv("model-view-text spacer", null, "", model_view); // Error while loading if (model_info.model_uuid == failedModelUUID) { newDiv("model-view-text divider", null, "", model_view); newDiv("model-view-text spacer", null, "", model_view); newDiv("model-view-text spacer", null, "", model_view); addModelItem("Error", failedModelError, "error"); } // Restore focus restoreFocus(focus); } function showModel(modelID) { if (modelID == "new") currentModelUUID = null; else currentModelUUID = modelID; let packet = {}; packet.model_uuid = modelID; if (modelID == "new") { let model_info = {}; model_info.model_uuid = "new"; model_info.name = "New model"; showModel_(model_info); } else { // console.log(packet); fetch("/api/get_model_info", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(packet) }) .then(response => response.json()) .then(json => { //console.log(json); showModel_(json.model_info); }); } } function createUIElements() { let def = createMainMenuButton("mainmenu-model", "Model", "/static/gfx/icon_model.png", "model-page", enterModelPage ); createMainMenuButton("mainmenu-chat", "Chat", "/static/gfx/icon_chat.png", "chat-page", enterChatPage ); def.click(); } function disablePage(mode) { let page_disabled = document.getElementById('page-disabled'); let busy = document.getElementById('busy'); let loading = document.getElementById('loading'); if (mode == "busy") busy.style.display = 'flex'; else busy.style.display = 'none'; if (mode == "loading") loading.style.display = 'flex'; else loading.style.display = 'none'; page_disabled.style.display = 'flex'; keyboard_disabled = true; } function enablePage() { let page_disabled = document.getElementById('page-disabled'); page_disabled.style.display = 'none'; keyboard_disabled = false; } function setLoadingProgress(a, b) { var progressBar = document.getElementById('loading-progress'); percentage = 100 * (a / b); progressBar.style.width = percentage + '%'; //progressBar.innerHTML = percentage + '%'; } function send(api, packet, ok_func = null, fail_func = null) { fetch(api, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(packet) }) .then(response => response.json()) .then(json => { if (json.hasOwnProperty("result")) { if (ok_func && json.result == "ok") ok_func(json); if (fail_func && json.result == "fail") fail_func(); } else { //console.log(json); throw new Error("Bad response from server."); } }) .catch(error => { alert("Error: " + error); console.error('Error:', error); }); } function getTextBoxValue(id) { let d = document.getElementById(id); if (!d) return d; //console.log(id, d.value); return d.value; } function getDivValue(id) { let d = document.getElementById(id); if (!d) return d; return d.innerHTML; } function getComboBoxValue(id) { let d = document.getElementById(id); if (!d) return d; return d.value; } function getCheckboxValue(id) { let d = document.getElementById(id); if (!d) return d; return d.checked; } function newModel(json) { let uuid = json.new_model_uuid; if (uuid) { //console.log("new model", uuid); let model_list = document.getElementById('model-list'); let model_new = document.getElementById('model_new'); model_new.id = "model_" + uuid; model_new.innerHTML = ""; addIcon(model_new, "model-icon") newDiv(null, null, "

", model_new); model_new.dataset.modelID = uuid; addModel("New model", "new"); showModel(uuid); } else { showModel(currentModelUUID); } } function numerical(text, int, min, max) { let num = 0; if (int) num = parseInt(text, 10); else num = parseFloat(text); if (isNaN(num)) return num; if (num < min) return min; if (num > max) return max; return num; } function sendCurrentModelView() { let packet = {}; packet.model_uuid = currentModelUUID; packet.name = getDivValue("div-model-name"); if (packet.name == null || packet.name == "") packet.name = "Unnamed model"; packet.model_directory = getTextBoxValue("tb-model-view-directory"); let seq_len = numerical(getTextBoxValue("tb-model-seq-len"), true, 16, 1024*1024); let rope_scale = numerical(getTextBoxValue("tb-model-rope-scale"), false, 0.01, 1024); let rope_alpha = numerical(getTextBoxValue("tb-model-rope-alpha"), false, 0.01, 1024); if (!isNaN(seq_len)) packet.seq_len = seq_len; if (!isNaN(rope_scale)) packet.rope_scale = rope_scale; if (!isNaN(rope_alpha)) packet.rope_alpha = rope_alpha; let cache_mode = getComboBoxValue("cb-model-cache-mode"); let chunk_size = numerical(getTextBoxValue("tb-model-chunk-size"), true, 128, 65536); let gpu_split_auto = getCheckboxValue("cb-model-gpu-split-auto"); let gpu_split = getTextBoxValue("tb-model-gpu-split"); if (cache_mode) packet.cache_mode = cache_mode; if (chunk_size) packet.chunk_size = chunk_size; if (gpu_split_auto != null) packet.gpu_split_auto = gpu_split_auto; if (gpu_split != null) packet.gpu_split = gpu_split; let draft_enabled = getCheckboxValue("cb-model-spec-dec"); packet.draft_enabled = draft_enabled; if (draft_enabled) { packet.draft_model_directory = getTextBoxValue("tb-model-view-draft-directory"); let draft_rope_alpha = numerical(getTextBoxValue("tb-model-draft-rope-alpha"), false, 0.01, 1024); let draft_rope_alpha_auto = getCheckboxValue("cb-model-draft-rope-alpha-auto"); if (draft_rope_alpha_auto != null) packet.draft_rope_alpha_auto = draft_rope_alpha_auto; if (!isNaN(rope_alpha)) packet.draft_rope_alpha = draft_rope_alpha; } //console.log(gpu_split_auto); console.log(packet); send("/api/update_model", packet, newModel); } function removeCurrentModel() { let packet = {}; packet.model_uuid = currentModelUUID; currentModelUUID = null; send("/api/remove_model", packet, enterModelPage); } function unloadCurrentModel() { disablePage("busy"); fetch("/api/unload_model") .then(response => response.json()) .then(json => { if (json.result == "ok") { loadedModelUUID = null; failedModelUUID = null; } enablePage(); enterModelPage(); }); } function loadCurrentModel() { setLoadingProgress(0, 1); disablePage("loading"); let packet = {}; packet.model_uuid = currentModelUUID; let timeout = new Promise((resolve, reject) => { let id = setTimeout(() => { clearTimeout(id); reject('No response from server') }, 10000) }); let fetchRequest = fetch("/api/load_model", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(packet) }); Promise.race([fetchRequest, timeout]) .then(response => { if (response.ok) { return response.body; } else { //appendErrorMessage("Network response was not ok"); 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) { enablePage(); enterModelPage(); 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 == "ok") { loadedModelUUID = packet.model_uuid; failedModelUUID = null; } else if (json.result == "progress") { setLoadingProgress(json.module, json.num_modules); //console.log(json); } else { loadedModelUUID = null; failedModelUUID = packet.model_uuid; failedModelError = json.error; console.log(json); } } data = lines[lines.length - 1]; reader.read().then(process); }); }) .catch(error => { loadedModelUUID = null; failedModelUUID = packet.model_uuid; failedModelError = "" + error; console.error('Error:', error); enablePage(); enterModelPage(); }); } function enterChatPage(subsFunc = null) { fetch("/api/list_sessions") .then(response => response.json()) .then(json => { //console.log(json); if (!currentSessionUUID && json.current_session) currentSessionUUID = json.current_session; populateSessionList(json.sessions); if (subsFunc) subsFunc(); }); } function populateSessionList(session_names) { session_list = document.getElementById('session-list'); session_list.innerHTML = ""; for (let session_uuid in session_names) { if (session_names.hasOwnProperty(session_uuid)) { addSession(session_names[session_uuid], session_uuid); } } addSession("New session", "new"); let def = document.getElementById(currentSessionUUID ? "session_" + currentSessionUUID : "session_new"); def.click(); } function addSession(name, sessionID) { let session_list = document.getElementById('session-list'); var nd = newDiv("session-list-entry inactive", "session_" + sessionID, null, session_list); if (sessionID == "new") { addIcon(nd, "session-new-icon"); // } else if (sessionID == loadedSessionUUID) { // let svg = addIcon(nd, "model-loaded-icon"); // svg.classList.add("active"); } else { addIcon(nd, "session-icon"); } var ndi = newDiv(null, null, "

" + name + "

", nd); nd.dataset.sessionID = sessionID; nd.addEventListener('click', function(event) { let session_list = document.getElementById('session-list'); let refresh = false; for (let i = 0; i < session_list.children.length; i++) { let c = session_list.children[i]; if (c.nodeName === 'DIV') { if (c == nd) { if (c.classList.contains("active")) { renameSession(c.dataset.sessionID); } else { refresh = true; c.classList.add("active"); c.classList.remove("inactive"); } } else { c.classList.add("inactive"); c.classList.remove("active"); } } } if (refresh) showSession(nd.dataset.sessionID); }); } let lockEdit = false; function renameSession(sessionID) { if (lockEdit) return; lockEdit = true; let div = document.getElementById("session_" + sessionID); div = div.children[1].children[0]; let prev = div.innerHTML; makeEditable(div, function(id, val) { if (val.trim() != "") { if (sessionID == "new") { fetch("/api/get_default_settings") .then(response => response.json()) .then(json => { let packet = {}; packet.settings = json.settings; packet.new_name = val; fetch("/api/new_session", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(packet) }) .then(response => response.json()) .then(json => { currentSessionUUID = json.session.session_uuid; enterChatPage(function() { } ); }); }); } else { let packet = {}; packet.session_uuid = sessionID; packet.new_name = val; send("/api/rename_session", packet); } } else { div.innerHTML = prev; } }, function() { lockEdit = false; }); } function showSession(sessionID) { //console.log("sessionID:", sessionID) if (sessionID != "new") { currentSessionUUID = sessionID; let packet = {}; packet.session_uuid = sessionID; // console.log(packet); fetch("/api/set_session", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(packet) }) .then(response => response.json()) .then(json => { //console.log(json); showSession_(json); }); } else { currentSessionUUID = null; showSession_(null); } } function showSession_(json) { // console.log("xxxxxxxxxxx", json); let session_view = document.getElementById("session-view-history"); session_view.innerHTML = ""; let settings = null; if (json) { settings = json.session.settings; currentSettings = settings; prompt_formats = json.prompt_formats; } if (json) { let history = json.session.history; for (let i = 0; i < history.length; i++) { setChatBlock(history[i]); } } if (settings) { populateSessionSettings(settings, prompt_formats); } else { fetch("/api/get_default_settings") .then(response => response.json()) .then(json => { populateSessionSettings(json.settings, json.prompt_formats); }); } document.getElementById('session-input').value = ""; document.getElementById('session-input').focus(); scrollToBottom(); } function setChatBlock(block, busy_anim = false) { let uuid = block.block_uuid; let nd = document.getElementById('chat_block_' + uuid); if (!nd) { let session_view = document.getElementById("session-view-history"); nd = document.createElement('div'); nd.id = 'chat_block_' + uuid; nd.className = "session-block"; session_view.appendChild(nd); let ndl = newDiv("vflex", null, null, nd, null); let nda = newDiv("avatar-img", null, null, ndl, null); let ndt = newDiv("session-block-text", null, null, ndl, null); nda.setAttribute("data-label", "avatar-img"); ndt.setAttribute("data-label", "text"); } setChatBlockAvatarImg(nd, block); setChatBlockText(nd, block, busy_anim); setChatBlockActions(nd, block); setChatBlockMeta(nd, block); if (block.author == "user") nd.classList.add("user"); } function setChatBlockMeta(nd, block) { let textdiv = nd.querySelector('.session-block-text'); if (Object.keys(block).includes("meta") && block.meta) { if (block.meta.overflow > 0) { let p = document.createElement('p'); p.classList.add("error"); p.innerHTML = "‼ Response exceeded " + block.meta.overflow + " tokens and was cut short."; textdiv.appendChild(p); } let p = document.createElement('p'); p.classList.add("meta"); if (!statsvisible) p.classList.add("hidden"); let ptps = block.meta.prompt_speed.toFixed(2) if (block.meta.prompt_speed > 50000) ptps = "∞"; let html = "prompt: " + block.meta.prompt_tokens.toFixed(0) + " tokens, " + ptps + " tokens/s"; html += " ⁄ "; html += "response: " + block.meta.gen_tokens.toFixed(0) + " tokens, " + block.meta.gen_speed.toFixed(2) + " tokens/s"; p.innerHTML = html; textdiv.appendChild(p); } } function setChatBlockActions(nd, block) { let uuid = block.block_uuid; let actdiv = document.createElement('div'); actdiv.classList.add("block-actions"); actdiv.classList.add("no-select"); let span = document.createElement("span"); span.classList.add("action"); span.innerHTML = "✕ Delete"; actdiv.appendChild(span); span.addEventListener('click', function() { let packet = {}; packet.block_uuid = uuid; send("/api/delete_block", packet, function() { let d = document.getElementById('chat_block_' + uuid); d.remove(); document.getElementById('session-input').focus(); }); }); span = document.createElement("span"); span.classList.add("action"); span.innerHTML = "🖉 Edit"; actdiv.appendChild(span); span.addEventListener('click', function() { //console.log(nd.dataset.rawtext); actdiv.parentNode.classList.add("hidden_h"); let textdiv = nd.querySelector('.session-block-text'); makeEditable(textdiv, function(id, val) { // console.log("edit"); let new_block = { ...block }; new_block.text = val.trim(); new_block.meta = null; let packet = {}; packet.block = new_block; send("/api/edit_block", packet, function() { new_block = block; new_block.text = val; new_block.meta = null; setChatBlockText(nd, new_block); setChatBlockMeta(nd, new_block); setChatBlockAvatarImg(nd, new_block); actdiv.parentNode.classList.remove("hidden_h"); document.getElementById('session-input').focus(); }); }, function(id) { // console.log("not edit"); setChatBlockText(nd, block); setChatBlockMeta(nd, block); actdiv.parentNode.classList.remove("hidden_h"); document.getElementById('session-input').focus(); }, in_text = nd.dataset.rawtext, multiline = true); }); //↻ nd.appendChild(actdiv); } function getRoleID(block) { if (!currentSettings) return -1; let t = block.text.toUpperCase(); for (let i = 0; i < 8; i++) { if (t.startsWith(currentSettings.roles[i].toUpperCase() + ":")) return i; } return -1; } function setChatBlockAvatarImg(nd, block) { let graphic = fallbackAvatar; if (currentSettings.prompt_format == "Chat-RP") { let ri = getRoleID(block); if (ri != -1) graphic = roleAvatars[ri]; } else { if (block.author == "user") graphic = instructAvatars[0]; if (block.author == "assistant") graphic = instructAvatars[1]; } let nda = nd.querySelector('div[data-label="avatar-img"]'); nda.innerHTML = ""; } function setChatBlockText(nd, block, busy_anim = false) { let nda = nd.querySelector('div[data-label="text"]'); let name = null; let col = fallbackColor; let text = block.text.trimEnd(); if (currentSettings.prompt_format == "Chat-RP") { let ri = getRoleID(block); if (ri != -1) { col = roleColors[ri]; name = currentSettings.roles[ri]; text = text.slice(name.length + 1).trimStart(); } } else { if (block.author == "user") { col = instructColors[0]; name = "User"; } if (block.author == "assistant") { col = instructColors[1]; name = "Assistant"; } } let html = ""; if (name) html += "
" + name + "
" if (busy_anim) { // html += "
" } html += marked.parse(text); nda.innerHTML = html; nd.dataset.rawtext = block.text.trimEnd(); //nd.dataset.rawblock = JSON.stringify(block); } function addSessionSettingsSection(heading, id = null) { let div = document.createElement('div'); div.className = "sss-block"; if (id) div.id = id; let div_head = document.createElement('div'); div_head.className = "sss-block-header no-select"; div_head.innerHTML = "" + heading; div.appendChild(div_head); div_head.addEventListener('click', function() { let c = this.nextElementSibling; let a = this.children[0]; if (c.classList.contains("shown")) { a.innerHTML = "⯈"; c.classList.remove("shown"); } else { a.innerHTML = "⯆"; c.classList.add("shown"); } }); let div_contents = document.createElement('div'); div_contents.className = "sss-block-contents shown"; div.appendChild(div_contents); let nd = document.getElementById("session-settings"); nd.appendChild(div); return div_contents; } function addSessionSettings_combo(parent, left, id = null, options = null, selected = null, editFunc = null) { let div = document.createElement('div'); div.className = "sss-item-split"; let h = document.createElement('div'); h.className = "sss-item-left"; h.innerHTML = left; let cb = document.createElement('select'); cb.className = "sss-item-right sss-item-combobox"; if (id) cb.id = id; for (let i = 0; i < options.length; i++) cb.add(new Option(options[i], options[i])); cb.value = selected; if (editFunc) cb.addEventListener("change", function(event) { editFunc(event) } ); div.appendChild(h); div.appendChild(cb); parent.appendChild(div); return cb; } function addSessionSettings_sep(parent) { let div = document.createElement('div'); div.className = "sss-item-sep"; parent.appendChild(div); } function addSessionSettings_textbox(parent, left, id = null, value = null, editFunc = null, rows = 1, right = null, rightFunc = null) { let div = document.createElement('div'); div.className = "sss-item-split"; let h = document.createElement('div'); h.className = "sss-item-left"; h.innerHTML = left; let tb = document.createElement('input'); tb.type= "text"; if (right) tb.className = "sss-item-mid sss-item-textbox"; else tb.className = "sss-item-right sss-item-textbox"; tb.rows = rows; tb.value = value; tb.spellcheck = false; if (id) tb.id = id; if (editFunc) tb.addEventListener("change", function(event) { editFunc(event) } ); let r = null; if (right) { r = document.createElement('div'); r.className = "sss-item-right"; r.innerHTML = right; if (rightFunc) { r.classList.add("clickable"); r.addEventListener("click", function(event) { rightFunc(event) } ); } } div.appendChild(h); div.appendChild(tb); if (right) div.appendChild(r); parent.appendChild(div); return tb; } function addSessionSettings_bigTextbox(parent, id = null, value = null, editFunc = null) { let tb = document.createElement('textarea'); tb.className = "sss-item-big-textbox"; tb.rows = 6; tb.value = value; if (id) tb.id = id; if (editFunc) tb.addEventListener("change", function(event) { editFunc(event) } ); parent.appendChild(tb); return tb; } function addSessionSettings_link(parent, text, id = null, rightFunc = null) { let div = document.createElement('div'); div.className = "sss-item-split"; let r = document.createElement('div'); r.className = "sss-item-left"; r.innerHTML = text; if (id != null) r.id = id; if (rightFunc) { r.classList.add("clickable"); r.addEventListener("click", function(event) { rightFunc(event) } ); } div.appendChild(r); parent.appendChild(div); } function addSessionSettings_slider(parent, id, left, minimum, maximum, is_float, editFunc, special = null) { let div = document.createElement('div'); div.className = "sss-item-split"; let h = document.createElement('div'); h.className = "sss-item-left"; h.innerHTML = left; let slider = document.createElement('input'); slider.type= "range"; slider.className = "sss-item-mid"; slider.min = minimum * (is_float ? 100 : 1); slider.max = maximum * (is_float ? 100 : 1); slider.value = slider.min; if (id) slider.id = id; let tb = document.createElement('input'); tb.type= "text"; tb.className = "sss-item-right sss-item-textbox-r"; tb.rows = 1; tb.value = is_float ? "0.0" : "0"; tb.spellcheck = false; if (id) tb.id = id + "_tb"; if (is_float) slider.dataset.is_float = "f"; else slider.dataset.is_float = "i"; slider.addEventListener("input", function(event) { let v = parseFloat(slider.value); let t = ""; if (is_float) { t = (v / 100.0).toFixed(2); } else { t = v.toFixed(0); } let st = ""; if (special) { if (Object.keys(special).includes(t)) st = special[t]; } if (st != "") { tb.value = st; tb.classList.add("special"); } else { tb.value = t; tb.classList.remove("special"); } }); slider.addEventListener("change", function(event) { if (editFunc) editFunc(event); }); let originalValue = ""; tb.addEventListener("focus", function(event) { originalValue = tb.value; }); tb.addEventListener("blur", function(event) { let nvalue = parseFloat(tb.value); if (isNaN(nvalue)) { tb.value = originalValue; return; } if (nvalue < minimum) nvalue = minimum; if (nvalue > maximum) nvalue = maximum; if (is_float) nvalue *= 100; slider.value = "" + nvalue; slider.dispatchEvent(new Event('input')); slider.dispatchEvent(new Event('change')); }); div.appendChild(h); div.appendChild(slider); div.appendChild(tb); parent.appendChild(div); return slider; } function setSlider(id, value) { let slider = document.getElementById(id); slider.value = "" + slider.dataset.is_float == "f" ? value * 100 : value; slider.dispatchEvent(new Event('input')); } function readSlider(id) { let slider = document.getElementById(id); let v = parseFloat(slider.value); if (slider.dataset.is_float == "f") v /= 100.0; return v; } function addSessionSettings_checkbox(parent, id, left, state, editFunc) { let div = document.createElement('div'); div.className = "sss-item-split"; let ndri = document.createElement('div'); ndri.className = "checkbox"; var cb = document.createElement('input'); cb.type = 'checkbox'; cb.checked = state; if (id) cb.id = id; var label = document.createElement('label'); if (id) label.htmlFor = id; label.appendChild(document.createTextNode(left)); if (editFunc) cb.addEventListener("change", function(event) { editFunc() } ); ndri.appendChild(cb); ndri.appendChild(label); div.appendChild(ndri); parent.appendChild(div); } function populateSessionSettings(settings, prompt_formats) { //console.log("xxxx", prompt_formats); let nd = document.getElementById("session-settings"); nd.innerHTML = ""; let div = null; div = addSessionSettingsSection("Prompt format", "sss-prompt-format"); addSessionSettings_combo(div, "Format", "sss-prompt-format-format", prompt_formats, "", updateSessionSettings); div = addSessionSettingsSection("Roles", "sss-roles"); addSessionSettings_textbox(div, "User", "sss-role-0", "", updateSessionSettings, 1, "

", null); addSessionSettings_textbox(div, "Bot #1", "sss-role-1", "", updateSessionSettings, 1, "

", null ); for (let i = 2; i < 8; i++) { addSessionSettings_textbox(div, "Bot #" + i, "sss-role-" + i, "", updateSessionSettings, 1, "✕ Remove", function(event) { //console.log(i); for (let j = i; j < 8; j++) { document.getElementById("sss-role-" + j).value = (j == 7 ? "" : document.getElementById("sss-role-" + (j + 1)).value); } updateSessionSettings(); }); } addSessionSettings_link(div, "+ Add...", "sss-role-add", function(event) { for (let j = 1; j < 8; j++) { tb = document.getElementById("sss-role-" + j); if (tb.value == "") { tb.value = "Assistant " + j; break; } } updateSessionSettings(); }); div = addSessionSettingsSection("System prompt", "sss-system-prompt"); addSessionSettings_bigTextbox(div, "system-prompt", "", updateSessionSettings); div = addSessionSettingsSection("Generation parameters", "sss-generation-params"); addSessionSettings_slider(div, "sss-slider-maxtokens", "Max tokens", 16, 2048, false, updateSessionSettings); addSessionSettings_slider(div, "sss-slider-chunktokens", "Chunk tokens", 16, 2048, false, updateSessionSettings); div = addSessionSettingsSection("Sampling", "sss-sampling"); addSessionSettings_slider(div, "sss-slider-temperature", "Temperature", 0, 3, true, updateSessionSettings); addSessionSettings_slider(div, "sss-slider-topk", "Top K", 0, 1000, false, updateSessionSettings, { "0": "off" } ); addSessionSettings_slider(div, "sss-slider-topp", "Top P", 0, 1, true, updateSessionSettings, { "0.00": "off", "1.00": "off" }); addSessionSettings_slider(div, "sss-slider-typical", "Typical", 0, 1, true, updateSessionSettings, { "0.00": "off", "1.00": "off" }); addSessionSettings_slider(div, "sss-slider-repp", "Rep. penalty", 1, 3, true, updateSessionSettings, { "1.00": "off" }); addSessionSettings_slider(div, "sss-slider-repr", "Rep. range", 0, 4096, false, updateSessionSettings); div = addSessionSettingsSection("Stop conditions", "sss-stop-conditions"); addSessionSettings_checkbox(div, "sss-checkbox-stopnewline", "Stop on newline", false, updateSessionSettings); if (settings) { setSessionSettings(settings); updateSessionSettings(null, false); } // Session controls nd = document.getElementById("session-list-controls"); nd.innerHTML = ""; if (currentSessionUUID) { let ndb = document.createElement("div"); let ndbi = document.createElement("span"); ndbi.innerHTML = "✖ Delete session"; ndbi.classList.add("linkbutton"); ndbi.addEventListener('click', function() { if (ndbi.classList.contains("danger")) { let packet = {}; packet.session_uuid = currentSessionUUID; currentSessionUUID = null; send("/api/delete_session", packet, function() { enterChatPage( function() { document.getElementById('session-input').focus(); }); }); } else { let saveHTML = ndbi.innerHTML; ndbi.innerHTML = "✖ Confirm"; ndbi.classList.add("danger"); setTimeout(function() { ndbi.classList.remove("danger"); ndbi.innerHTML = saveHTML; }, 2000); } }); nd.appendChild(ndb); ndb.appendChild(ndbi); } //nd.appendChild(document.createElement("p")); createSessionControlCheckbox(nd, "cb-show-stats", "Show generation stats", statsvisible, showHideStats); } function showHideStats() { statsvisible = document.getElementById("cb-show-stats").checked; var divs = document.getElementsByClassName("meta"); for(var i = 0; i < divs.length; i++) { if (statsvisible) divs[i].classList.remove("hidden"); else divs[i].classList.add("hidden"); } } function createSessionControlCheckbox(parent, id, text, state, editFunc = null) { let div = document.createElement('div'); let ndri = document.createElement('div'); ndri.className = "checkbox-sc"; var cb = document.createElement('input'); cb.type = 'checkbox'; cb.checked = state; if (id) cb.id = id; var label = document.createElement('label'); if (id) label.htmlFor = id; label.appendChild(document.createTextNode(text)); if (editFunc) cb.addEventListener("change", function(event) { editFunc() } ); ndri.appendChild(cb); ndri.appendChild(label); div.appendChild(ndri); parent.appendChild(div); } function setSessionSettings(settings) { //console.log(settings); document.getElementById("sss-prompt-format-format").value = settings.prompt_format; for (let i = 0; i < 8; i++) document.getElementById("sss-role-" + i).value = settings.roles[i]; document.getElementById("system-prompt").value = settings.system_prompt; setSlider("sss-slider-maxtokens", settings.maxtokens); setSlider("sss-slider-chunktokens", settings.chunktokens); document.getElementById("sss-checkbox-stopnewline").checked = settings.stop_newline ?? false; setSlider("sss-slider-temperature", settings.temperature ?? 0.8); setSlider("sss-slider-topk", settings.top_k ?? 50); setSlider("sss-slider-topp", settings.top_p ?? 0.8); setSlider("sss-slider-typical", settings.typical ?? 0.0); setSlider("sss-slider-repp", settings.repp ?? 1.15); setSlider("sss-slider-repr", settings.repr ?? 1024); } function updateSessionSettings(event, send_updates = true) { let focus = saveFocus(); let settings = {}; settings.prompt_format = document.getElementById("sss-prompt-format-format").value; // Roles if (settings.prompt_format == "Chat-RP") { document.getElementById("sss-roles").style.display = ""; document.getElementById("sss-stop-conditions").style.display = ""; } else { document.getElementById("sss-roles").style.display = "none"; document.getElementById("sss-stop-conditions").style.display = "none"; } let roles = []; let numroles = 1; for (let i = 0; i < 8; i++) roles.push(document.getElementById("sss-role-" + i).value.trim()); //console.log(roles); if (roles[0] == "") roles[0] = "User"; for (let i = 1; i < 8; i++) if (roles[i] != "" && i + 1 > numroles) numroles = i + 1; for (let i = 2; i < numroles; i++) if (roles[i] == "") roles[i] = "Assistant " + i; for (let i = 0; i < 8; i++) { document.getElementById("sss-role-" + i).parentNode.style.display = (i < numroles) ? "" : "none"; } document.getElementById("sss-role-add").style.display = (numroles < 8 ? "" : "none"); settings.roles = roles; // System prompt settings.system_prompt = document.getElementById("system-prompt").value.trim(); // Generation params settings.maxtokens = readSlider("sss-slider-maxtokens"); settings.chunktokens = readSlider("sss-slider-chunktokens"); settings.stop_newline = document.getElementById("sss-checkbox-stopnewline").checked; // Sampling settings.temperature = readSlider("sss-slider-temperature"); settings.top_k = readSlider("sss-slider-topk"); settings.top_p = readSlider("sss-slider-topp"); settings.typical = readSlider("sss-slider-typical"); settings.repp = readSlider("sss-slider-repp"); settings.repr = readSlider("sss-slider-repr"); // Update history if prompt format changes let updateHistory = false; if (!currentSettings || currentSettings.prompt_format != settings.prompt_format) updateHistory = true; // Update currentSettings = settings; // Send to server let packet = {}; packet.settings = settings; if (send_updates) { if (currentSessionUUID) { send("/api/update_settings", packet, function() { if (updateHistory) showSession(currentSessionUUID); } ); } else { fetch("/api/new_session", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(packet) }) .then(response => response.json()) .then(json => { currentSessionUUID = json.session.session_uuid; enterChatPage(function() { restoreFocus(focus); } ); }); } } } function saveFocus() { let focus = {}; focus.focusedDiv = document.activeElement; focus.focusedID = null; focus.selectionStart = -1; focus.selectionEnd = -1; if (focus.focusedDiv) { focus.focusedID = focus.focusedDiv.id; if (focus.focusedDiv.tagName === 'INPUT' && focus.focusedDiv.type === 'text') { focus.selectionStart = focus.focusedDiv.selectionStart; focus.selectionEnd = focus.focusedDiv.selectionEnd; } } // console.log(focus); return focus; } function restoreFocus(focus) { // console.log("restore:", focus); if (focus.focusedID && focus.focusedID != "") { let focusedElement = document.getElementById(focus.focusedID); if (focusedElement) { focusedElement.focus(); if (focus.selectionStart != -1) { focusedElement.selectionStart = focus.selectionStart; focusedElement.selectionEnd = focus.selectionEnd; } } } } function submitInput() { const textarea = document.getElementById('session-input'); input = textarea.value.trim(); textarea.value = ""; scrollToBottom(); if (currentSessionUUID) { submitInput_(input); } else { fetch("/api/get_default_settings") .then(response => response.json()) .then(json => { let packet = {}; packet.settings = json.settings; fetch("/api/new_session", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(packet) }) .then(response => response.json()) .then(json => { currentSessionUUID = json.session.session_uuid; enterChatPage(function() { submitInput_(input) } ); }); }); } } function submitInput_(input) { if (input == "") { getResponse(); } else if (loadedModelUUID) { supplyUserInput(input, getResponse); } else { supplyUserInput(input); } } function supplyUserInput(input, responseFunc = null) { let packet = {}; packet.user_input_text = input; fetch("/api/user_input", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(packet) }) .then(response => response.json()) .then(json => { setChatBlock(json.new_block); if (responseFunc) responseFunc(); }); } function getResponse() { let packet = {}; let timeout = new Promise((resolve, reject) => { let id = setTimeout(() => { clearTimeout(id); reject('No response from server') }, 10000) }); let fetchRequest = fetch("/api/generate", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(packet) }); Promise.race([fetchRequest, timeout]) .then(response => { if (response.ok) { return response.body; } else { //appendErrorMessage("Network response was not ok"); 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"); 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); return; } else { receivedStreamResponse(json); } } data = lines[lines.length - 1]; reader.read().then(process); }); }) .catch(error => { console.error('Error:', error); }); } function receivedStreamResponse(json) { // sticky = isNearBottom(); sticky = true; if (json.result == "begin_block") { setChatBlock(json.block, true); currentStreamingBlock = json.block; } if (json.result == "prompt_eval") { let uuid = currentStreamingBlock.block_uuid; let nd = document.getElementById('chat_block_' + uuid); nd.children[0].children[1].innerHTML += "
"; } if (json.result == "stream_to_block") { currentStreamingBlock.text += json.text; let nd = document.getElementById('chat_block_' + json.block_uuid); setChatBlockText(nd, currentStreamingBlock); } if (json.result == "ok") { let nd = document.getElementById('chat_block_' + json.new_block.block_uuid); setChatBlockText(nd, json.new_block); setChatBlockMeta(nd, json.new_block); } if (sticky) scrollToBottom(); } function isNearBottom() { const element = document.getElementById('session-view-history'); const threshold = 10; const position = element.scrollTop + element.offsetHeight; const height = element.scrollHeight; console.log(element.scrollTop, element.offsetHeight, element.scrollTop + element.offsetHeight, element.scrollHeight); return height - position < threshold; } function scrollToBottom() { requestAnimationFrame(() => { requestAnimationFrame(() => { const element = document.getElementById('session-view-history'); element.scroll({ top: element.scrollHeight, behavior: 'smooth' }); element.scroll({ top: element.scrollHeight, behavior: 'smooth' }); // ?? }); }); }