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;