1
0
mirror of https://github.com/coalaura/whiskr.git synced 2025-12-02 20:22:52 +00:00

floating sticky header

This commit is contained in:
Laura
2025-11-30 22:27:11 +01:00
parent ebaa26c81d
commit 4d1bdf981a
2 changed files with 160 additions and 137 deletions

View File

@@ -215,17 +215,25 @@ body:not(.loading) #loading {
gap: 40px; gap: 40px;
height: 100%; height: 100%;
overflow-y: auto; overflow-y: auto;
padding: 14px 12px; padding: 0 12px;
padding-bottom: 20px; padding-bottom: 20px;
overflow-anchor: none; overflow-anchor: none;
} }
#messages::before {
content: "";
display: block;
height: 14px;
flex-shrink: 0;
margin-bottom: -40px;
}
#messages:empty::before { #messages:empty::before {
content: "whiskr - no messages"; content: "whiskr - no messages";
color: var(--c-subtext); color: var(--c-subtext);
font-style: italic; font-style: italic;
align-self: center; align-self: center;
margin-top: 5px; margin-top: 20px;
} }
#message, #message,
@@ -242,9 +250,11 @@ body:not(.loading) #loading {
max-width: min(700px, 100%); max-width: min(700px, 100%);
min-width: 280px; min-width: 280px;
width: max-content; width: max-content;
padding-top: 28px; background: transparent;
background: var(--c-surface0);
flex-shrink: 0; flex-shrink: 0;
display: flex;
flex-direction: column;
gap: 4px;
} }
.message::after { .message::after {
@@ -259,6 +269,7 @@ body:not(.loading) #loading {
bottom: 0; bottom: 0;
transition: opacity 150ms; transition: opacity 150ms;
border-radius: 6px; border-radius: 6px;
z-index: 15;
} }
.message.marked::after { .message.marked::after {
@@ -273,16 +284,25 @@ body:not(.loading) #loading {
align-self: center; 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 { .message .role {
position: absolute;
display: flex; display: flex;
gap: 4px; gap: 4px;
align-items: center; align-items: center;
font-family: var(--font-mono); font-family: var(--font-mono);
font-size: 12px; font-size: 12px;
line-height: 12px; line-height: 12px;
top: 6px;
left: 6px;
} }
.statistics .provider::after, .statistics .provider::after,
@@ -425,8 +445,7 @@ body:not(.loading) #loading {
.message .body { .message .body {
position: relative; position: relative;
border-bottom-left-radius: 6px; border-radius: 6px;
border-bottom-right-radius: 6px;
overflow: hidden; overflow: hidden;
padding: 14px 12px; padding: 14px 12px;
display: flex; display: flex;
@@ -584,12 +603,11 @@ body:not(.loading) #loading {
.message .options { .message .options {
display: flex; display: flex;
gap: 4px; gap: 4px;
position: absolute;
top: 4px;
right: 6px;
opacity: 0; opacity: 0;
pointer-events: none; pointer-events: none;
transition: 150ms; transition: 150ms;
background: var(--c-surface0);
border-radius: 4px;
} }
.message.editing .options, .message.editing .options,

View File

@@ -253,10 +253,15 @@
// main message div // main message div
this.#_message = make("div", "message", this.#role, collapsed ? "collapsed" : ""); this.#_message = make("div", "message", this.#role, collapsed ? "collapsed" : "");
// header
const _header = make("div", "header");
this.#_message.appendChild(_header);
// message role (wrapper) // message role (wrapper)
const _wrapper = make("div", "role", this.#role); const _wrapper = make("div", "role", this.#role);
this.#_message.appendChild(_wrapper); _header.appendChild(_wrapper);
observer.observe(_wrapper); observer.observe(_wrapper);
@@ -272,6 +277,130 @@
_wrapper.appendChild(this.#_tags); _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 // message body
const _body = make("div", "body"); const _body = make("div", "body");
@@ -408,130 +537,6 @@
this.#_tool.appendChild(_callResult); 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 // statistics
this.#_statistics = make("div", "statistics"); this.#_statistics = make("div", "statistics");