From 4d1bdf981aba4e82f56b04d2162d2036d753d050 Mon Sep 17 00:00:00 2001 From: Laura Date: Sun, 30 Nov 2025 22:27:11 +0100 Subject: [PATCH] floating sticky header --- static/css/chat.css | 42 +++++--- static/js/chat.js | 255 ++++++++++++++++++++++---------------------- 2 files changed, 160 insertions(+), 137 deletions(-) diff --git a/static/css/chat.css b/static/css/chat.css index 6feab8d..9cda869 100644 --- a/static/css/chat.css +++ b/static/css/chat.css @@ -215,17 +215,25 @@ body:not(.loading) #loading { gap: 40px; height: 100%; overflow-y: auto; - padding: 14px 12px; + padding: 0 12px; padding-bottom: 20px; overflow-anchor: none; } +#messages::before { + content: ""; + display: block; + height: 14px; + flex-shrink: 0; + margin-bottom: -40px; +} + #messages:empty::before { content: "whiskr - no messages"; color: var(--c-subtext); font-style: italic; align-self: center; - margin-top: 5px; + margin-top: 20px; } #message, @@ -242,9 +250,11 @@ body:not(.loading) #loading { max-width: min(700px, 100%); min-width: 280px; width: max-content; - padding-top: 28px; - background: var(--c-surface0); + background: transparent; flex-shrink: 0; + display: flex; + flex-direction: column; + gap: 4px; } .message::after { @@ -259,6 +269,7 @@ body:not(.loading) #loading { bottom: 0; transition: opacity 150ms; border-radius: 6px; + z-index: 15; } .message.marked::after { @@ -273,16 +284,25 @@ body:not(.loading) #loading { align-self: center; } +.message .header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 4px 6px; + background: var(--c-surface0); + position: sticky; + top: 0; + z-index: 10; + border-radius: 6px; +} + .message .role { - position: absolute; display: flex; gap: 4px; align-items: center; font-family: var(--font-mono); font-size: 12px; line-height: 12px; - top: 6px; - left: 6px; } .statistics .provider::after, @@ -425,8 +445,7 @@ body:not(.loading) #loading { .message .body { position: relative; - border-bottom-left-radius: 6px; - border-bottom-right-radius: 6px; + border-radius: 6px; overflow: hidden; padding: 14px 12px; display: flex; @@ -584,12 +603,11 @@ body:not(.loading) #loading { .message .options { display: flex; gap: 4px; - position: absolute; - top: 4px; - right: 6px; opacity: 0; pointer-events: none; transition: 150ms; + background: var(--c-surface0); + border-radius: 4px; } .message.editing .options, diff --git a/static/js/chat.js b/static/js/chat.js index 0b14587..8c75039 100644 --- a/static/js/chat.js +++ b/static/js/chat.js @@ -253,10 +253,15 @@ // main message div this.#_message = make("div", "message", this.#role, collapsed ? "collapsed" : ""); + // header + const _header = make("div", "header"); + + this.#_message.appendChild(_header); + // message role (wrapper) const _wrapper = make("div", "role", this.#role); - this.#_message.appendChild(_wrapper); + _header.appendChild(_wrapper); observer.observe(_wrapper); @@ -272,6 +277,130 @@ _wrapper.appendChild(this.#_tags); + // message options + const _opts = make("div", "options"); + + _header.appendChild(_opts); + + // collapse option + const _optCollapse = make("button", "collapse"); + + _optCollapse.title = "Collapse/Expand message"; + + _opts.appendChild(_optCollapse); + + _optCollapse.addEventListener("click", () => { + this.#_message.classList.toggle("collapsed"); + + updateScrollButton(); + + setFollowTail(distanceFromBottom() <= nearBottom); + + this.#save(); + }); + + // attach option + if (this.#role === "user") { + const _attach = make("button", "attach"); + + _attach.title = "Add files to this message"; + + _opts.appendChild(_attach); + + _attach.addEventListener("click", () => { + uploadToMessage(_attach, this); + }); + } + + // copy option + const _optCopy = make("button", "copy"); + + _optCopy.title = "Copy message content"; + + _opts.appendChild(_optCopy); + + let timeout; + + _optCopy.addEventListener("click", () => { + this.stopEdit(); + + clearTimeout(timeout); + + navigator.clipboard.writeText(this.#text); + + _optCopy.classList.add("copied"); + + timeout = setTimeout(() => { + _optCopy.classList.remove("copied"); + }, 1000); + }); + + // retry option + const _assistant = this.#role === "assistant", + _retryLabel = _assistant ? "Delete message and messages after this one and try again" : "Delete messages after this one and try again"; + + const _optRetry = make("button", "retry"); + + _optRetry.title = _retryLabel; + + _opts.appendChild(_optRetry); + + _optRetry.addEventListener("mouseenter", () => { + const index = this.index(_assistant ? 0 : 1); + + mark(index); + }); + + _optRetry.addEventListener("mouseleave", () => { + mark(false); + }); + + _optRetry.addEventListener("click", () => { + const index = this.index(_assistant ? 0 : 1); + + if (index === false) { + return; + } + + abortNow(); + + this.stopEdit(); + + while (messages.length > index) { + messages[messages.length - 1].delete(); + } + + mark(false); + + generate(false, true); + }); + + // edit option + const _optEdit = make("button", "edit"); + + _optEdit.title = "Edit message content"; + + _opts.appendChild(_optEdit); + + _optEdit.addEventListener("click", () => { + if (this.#_message.classList.contains("collapsed")) { + _optCollapse.click(); + } + + this.toggleEdit(); + }); + + // delete option + const _optDelete = make("button", "delete"); + + _optDelete.title = "Delete message"; + + _opts.appendChild(_optDelete); + + _optDelete.addEventListener("click", () => { + this.delete(); + }); + // message body const _body = make("div", "body"); @@ -408,130 +537,6 @@ this.#_tool.appendChild(_callResult); - // message options - const _opts = make("div", "options"); - - this.#_message.appendChild(_opts); - - // collapse option - const _optCollapse = make("button", "collapse"); - - _optCollapse.title = "Collapse/Expand message"; - - _opts.appendChild(_optCollapse); - - _optCollapse.addEventListener("click", () => { - this.#_message.classList.toggle("collapsed"); - - updateScrollButton(); - - setFollowTail(distanceFromBottom() <= nearBottom); - - this.#save(); - }); - - // attach option - if (this.#role === "user") { - const _attach = make("button", "attach"); - - _attach.title = "Add files to this message"; - - _opts.appendChild(_attach); - - _attach.addEventListener("click", () => { - uploadToMessage(_attach, this); - }); - } - - // copy option - const _optCopy = make("button", "copy"); - - _optCopy.title = "Copy message content"; - - _opts.appendChild(_optCopy); - - let timeout; - - _optCopy.addEventListener("click", () => { - this.stopEdit(); - - clearTimeout(timeout); - - navigator.clipboard.writeText(this.#text); - - _optCopy.classList.add("copied"); - - timeout = setTimeout(() => { - _optCopy.classList.remove("copied"); - }, 1000); - }); - - // retry option - const _assistant = this.#role === "assistant", - _retryLabel = _assistant ? "Delete message and messages after this one and try again" : "Delete messages after this one and try again"; - - const _optRetry = make("button", "retry"); - - _optRetry.title = _retryLabel; - - _opts.appendChild(_optRetry); - - _optRetry.addEventListener("mouseenter", () => { - const index = this.index(_assistant ? 0 : 1); - - mark(index); - }); - - _optRetry.addEventListener("mouseleave", () => { - mark(false); - }); - - _optRetry.addEventListener("click", () => { - const index = this.index(_assistant ? 0 : 1); - - if (index === false) { - return; - } - - abortNow(); - - this.stopEdit(); - - while (messages.length > index) { - messages[messages.length - 1].delete(); - } - - mark(false); - - generate(false, true); - }); - - // edit option - const _optEdit = make("button", "edit"); - - _optEdit.title = "Edit message content"; - - _opts.appendChild(_optEdit); - - _optEdit.addEventListener("click", () => { - if (this.#_message.classList.contains("collapsed")) { - _optCollapse.click(); - } - - this.toggleEdit(); - }); - - // delete option - const _optDelete = make("button", "delete"); - - _optDelete.title = "Delete message"; - - _opts.appendChild(_optDelete); - - _optDelete.addEventListener("click", () => { - this.delete(); - }); - // statistics this.#_statistics = make("div", "statistics");