diff --git a/static/koboldai.css b/static/koboldai.css index 35e861cb..39f3b21e 100644 --- a/static/koboldai.css +++ b/static/koboldai.css @@ -2150,6 +2150,43 @@ body { border-radius: var(--radius_settings_button); } +/* Context Menu */ +#context-menu { + position: absolute; + cursor: default; + + /* Nothing should be above the context menu (that I can think of) */ + z-index: 9999999; + + /* TODO: Theme */ + background-color: #222f3a; + border: 1px solid #485e6d; +} + +#context-menu > hr { + /* TODO: Theme */ + border-top: 2px solid #485e6d; + margin: 5px 5px; +} + +.context-menu-item { + padding: 5px; + padding-right: 25px; + min-width: 100px; +} + +.context-menu-item:hover { + /* TODO: Theme */ + background-color: #273b48; +} + +.context-menu-item > .material-icons-outlined { + position: relative; + top: 2px; + font-size: 15px; + margin-right: 5px; +} + /*---------------------------------- Global ------------------------------------------------*/ .hidden { display: none; diff --git a/static/koboldai.js b/static/koboldai.js index 78f1fb09..285fc76a 100644 --- a/static/koboldai.js +++ b/static/koboldai.js @@ -73,6 +73,22 @@ const finder_actions = [ // {name: "", icon: "palette", func: function() { highlightEl("#biasing") }}, ]; +const context_menu_actions = [ + {label: "Cut", icon: "content_cut", visibilityCondition: "SELECTION", click: cut}, + {label: "Copy", icon: "content_copy", visibilityCondition: "SELECTION", click: copy}, + {label: "Paste", icon: "content_paste", visibilityCondition: "SELECTION", click: paste}, + // Null makes a seperation bar + null, + {label: "Add to Memory", icon: "assignment", visibilityCondition: "SELECTION", click: push_selection_to_memory}, + {label: "Add to World Info Entry", icon: "auto_stories", visibilityCondition: "SELECTION", click: push_selection_to_world_info}, + {label: "Add as Bias", icon: "insights", visibilityCondition: "SELECTION", click: push_selection_to_phrase_bias}, + {label: "Retry from here", icon: "refresh", visibilityCondition: "CARET", click: retry_from_here}, + // Not implemented! See view_selection_probabiltiies + // null, + // {label: "View Token Probabilities", icon: "assessment", visibilityCondition: "SELECTION", click: view_selection_probabilities}, + // {label: "View Token Probabilities", icon: "account_tree", visibilityCondition: "SELECTION", click: view_selection_probabilities}, +]; + const map1 = new Map() map1.set('Top K Sampling', 0) @@ -2307,6 +2323,9 @@ function push_selection_to_phrase_bias() { } function retry_from_here() { + // TODO: Make this from the caret position (get_caret_position()) instead + // of per action. Actions may start out well, but go off the rails later, so + // we should be able to retry from any position. let chunk = null; for (element of document.getElementsByClassName("editing")) { if (element.id == 'story_prompt') { @@ -2328,16 +2347,25 @@ function retry_from_here() { } } +function view_selection_probabilities() { + // Not quite sure how this should work yet. Probabilities are obviously on + // the token level, which we have no UI representation of. There are other + // token-level visualization features I'd like to implement (like something + // for self-attention), so if that works out it might be best to have a + // modifier key (i.e. alt) enter a "token selection mode" when held. + console.log("Not implemented! :("); +} + function copy() { - document.execCommand("copy") + document.execCommand("copy"); } function paste() { - document.execCommand("paste") + document.execCommand("paste"); } function cut() { - document.execCommand("cut") + document.execCommand("cut"); } function getSelectionText() { @@ -2356,6 +2384,15 @@ function getSelectionText() { return text; } +function get_caret_position(target) { + if ( + document.activeElement !== target && + !$.contains(target, document.activeElement) + ) return null; + + return getSelection().focusOffset; +} + function show_save_preset() { document.getElementById("save_preset").classList.remove("hidden"); } @@ -3803,7 +3840,6 @@ function updateStandardSearchListings(query) { function $e(tag, parent, attributes) { // Small helper function for dynamic UI creation - // TODO: Support nested attributed with "." syntax. let element = document.createElement(tag); @@ -4124,6 +4160,34 @@ function process_cookies() { load_tweaks(); } +function position_context_menu(contextMenu, x, y) { + // Calculate where to position context menu based on window confines and + // menu size. + + let height = contextMenu.clientHeight; + let width = contextMenu.clientWidth; + + let bounds = { + top: 0, + bottom: window.innerHeight, + left: 0, + right: window.innerWidth, + }; + + let farMenuBounds = { + top: y, + bottom: y + height, + left: x, + right: x + width, + }; + + if (farMenuBounds.right > bounds.right) x -= farMenuBounds.right - bounds.right; + if (farMenuBounds.bottom > bounds.bottom) y -= farMenuBounds.bottom - bounds.bottom; + + contextMenu.style.left = `${x}px`; + contextMenu.style.top = `${y}px`; +} + $(document).ready(function(){ on_colab = document.getElementById("on_colab").textContent == "true"; @@ -4287,6 +4351,69 @@ $(document).ready(function(){ debugContainer.addEventListener("click", function(e) { debugContainer.classList.add("hidden"); }); + + // Context menu + const contextMenu = $e("div", document.body, {id: "context-menu"}); + + for (const action of context_menu_actions) { + // Null adds horizontal rule + if (!action) { + $e("hr", contextMenu); + continue; + } + + let item = $e("div", contextMenu, { + classes: ["context-menu-item", "noselect"], + "visibility-condition": action.visibilityCondition + }); + let icon = $e("span", item, {classes: ["material-icons-outlined"], innerText: action.icon}); + item.append(action.label); + + item.addEventListener("mousedown", (e) => (e.preventDefault())); + item.addEventListener("click", action.click); + } + + $("#gamescreen").contextmenu(function(event) { + // Don't open browser context menu + event.preventDefault(); + + // Close if open + if (!contextMenu.classList.contains("hidden")) { + contextMenu.classList.add("hidden"); + return; + } + + // Disable non-applicable items + $(".context-menu-item").addClass("disabled"); + + // A selection is made + if (getSelectionText()) $(".context-menu-item[visibility-condition=SELECTION]").removeClass("disabled"); + + // The caret is placed + if (get_caret_position($("#gamescreen")[0]) !== null) $(".context-menu-item[visibility-condition=CARET]").removeClass("disabled"); + + contextMenu.classList.remove("hidden"); + + // Set position to click position + position_context_menu(contextMenu, event.originalEvent.x, event.originalEvent.y); + + // Don't let the document contextmenu catch us and close our context menu + event.stopPropagation(); + }); + + // When we make a browser context menu, close ours. + $(document).contextmenu(function(event) { + contextMenu.classList.add("hidden"); + }); + + // When we click outside of our context menu, close ours. + $(document).click(function(event) { + contextMenu.classList.add("hidden"); + }); + + window.addEventListener("blur", function(event) { + contextMenu.classList.add("hidden"); + }); }); document.addEventListener("keydown", function(event) { diff --git a/templates/index_new.html b/templates/index_new.html index 07899e78..143a2802 100644 --- a/templates/index_new.html +++ b/templates/index_new.html @@ -137,7 +137,5 @@ - - \ No newline at end of file