diff --git a/README.md b/README.md index 69f57fb..b9bba21 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,6 @@ whiskr is a private, self-hosted web chat interface for interacting with AI mode ## TODO -- resizable chat box - cost tracker - settings - auto-retry on edit diff --git a/static/css/chat.css b/static/css/chat.css index e195a82..3b99e26 100644 --- a/static/css/chat.css +++ b/static/css/chat.css @@ -68,6 +68,11 @@ body { overflow: hidden; } +body.resizing * { + user-select: none !important; + cursor: grabbing !important; +} + #version { position: absolute; font-size: 12px; @@ -565,6 +570,7 @@ body:not(.loading) #loading { padding: 0 12px; height: 320px; padding-bottom: 36px; + flex-shrink: 0; } #chat::after { @@ -581,6 +587,15 @@ body:not(.loading) #loading { padding-top: 50px; } +#resize-bar { + position: absolute; + top: -4px; + left: 0; + right: 0; + height: 8px; + cursor: n-resize; +} + #attachments { position: absolute; top: 2px; diff --git a/static/index.html b/static/index.html index 57f7d91..f372c3a 100644 --- a/static/index.html +++ b/static/index.html @@ -29,6 +29,8 @@
+
+
diff --git a/static/js/chat.js b/static/js/chat.js index 1ac6ddc..aa0087c 100644 --- a/static/js/chat.js +++ b/static/js/chat.js @@ -4,6 +4,7 @@ $chat = document.getElementById("chat"), $message = document.getElementById("message"), $bottom = document.getElementById("bottom"), + $resizeBar = document.getElementById("resize-bar"), $attachments = document.getElementById("attachments"), $role = document.getElementById("role"), $model = document.getElementById("model"), @@ -37,7 +38,9 @@ searchTool = false; let searchAvailable = false, - activeMessage; + activeMessage = null, + isResizing = false, + scrollResize = false; function updateScrollButton() { const bottom = $messages.scrollHeight - ($messages.scrollTop + $messages.offsetHeight); @@ -49,7 +52,7 @@ } } - function scroll(force = false) { + function scroll(force = false, instant = false) { if (!autoScrolling && !force) { updateScrollButton(); @@ -59,7 +62,7 @@ setTimeout(() => { $messages.scroll({ top: $messages.scrollHeight, - behavior: "smooth", + behavior: instant ? "instant" : "smooth", }); }, 0); } @@ -1230,6 +1233,27 @@ scroll(true); }); + $resizeBar.addEventListener("mousedown", event => { + const isAtBottom = $messages.scrollHeight - ($messages.scrollTop + $messages.offsetHeight) <= 10; + + if (event.buttons === 4) { + $chat.style.height = ""; + + storeValue("resized", false); + + scroll(isAtBottom, true); + + return; + } else if (event.buttons !== 1) { + return; + } + + isResizing = true; + scrollResize = isAtBottom; + + document.body.classList.add("resizing"); + }); + $role.addEventListener("change", () => { storeValue("role", $role.value); }); @@ -1468,9 +1492,36 @@ } }); + addEventListener("mousemove", event => { + if (!isResizing) { + return; + } + + const total = window.innerHeight, + height = clamp(window.innerHeight - event.clientY, 100, total - 240); + + $chat.style.height = `${height}px`; + + storeValue("resized", height); + + scroll(scrollResize, true); + }); + + addEventListener("mouseup", () => { + isResizing = false; + + document.body.classList.remove("resizing"); + }); + dropdown($role); dropdown($reasoningEffort); + const resizedHeight = loadValue("resized"); + + if (resizedHeight) { + $chat.style.height = `${resizedHeight}px`; + } + loadData().then(() => { restore(); diff --git a/static/js/lib.js b/static/js/lib.js index 6c57573..9a7c52e 100644 --- a/static/js/lib.js +++ b/static/js/lib.js @@ -84,6 +84,10 @@ function fixed(num, decimals = 0) { return num.toFixed(decimals).replace(/\.?0+$/m, ""); } +function clamp(num, min, max) { + return Math.min(Math.max(num, min), max); +} + function download(name, type, data) { let blob;