mirror of
https://github.com/coalaura/whiskr.git
synced 2025-09-08 17:06:42 +00:00
improved auto-scroll and waiting indicator
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# Tool use
|
||||
Use at most 1 tool call per turn. You have %d turns with tool calls total.
|
||||
Use at most 1 tool call per turn. You have %d turns with tool calls total. You cannot call multiple tools at once. If you have multiple turns you can call more tools in later turns. Match the tool arguments exactly.
|
||||
|
||||
search_web({query, num_results?, intent?, recency?, domains?})
|
||||
- Fresh info & citations. Keep query short; add month/year if freshness matters.
|
||||
|
@@ -221,9 +221,23 @@ body:not(.loading) #loading {
|
||||
}
|
||||
|
||||
#title.refreshing #title-text {
|
||||
position: relative;
|
||||
filter: blur(3px);
|
||||
}
|
||||
|
||||
#title.refreshing #title-text::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 38px;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
animation: swivel 1.2s ease-in-out infinite;
|
||||
background: #6e738d;
|
||||
opacity: 0.5;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
#messages {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -320,6 +334,7 @@ body:not(.loading) #loading {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.message:not(.has-text) .text,
|
||||
.message:not(.has-tags) .tags {
|
||||
display: none;
|
||||
}
|
||||
@@ -395,6 +410,14 @@ body:not(.loading) #loading {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.reasoning-text strong {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.reasoning-text strong:not(:first-child) {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.message.has-reasoning:not(.has-text):not(.errored) div.text,
|
||||
.message.has-tool:not(.has-text):not(.errored) div.text,
|
||||
.message.has-files:not(.has-text):not(.errored) div.text,
|
||||
@@ -611,9 +634,30 @@ body:not(.loading) #loading {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.message:empty.receiving .text::before,
|
||||
.message.waiting .text::before {
|
||||
content: ". . .";
|
||||
.message .loader {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.message.receiving:not(.has-text) .loader,
|
||||
.message.waiting .loader {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.message .loader span {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #939ab7;
|
||||
animation: bounce 1.4s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.message .loader span:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.message .loader span:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
.statistics {
|
||||
@@ -710,6 +754,10 @@ body:not(.loading) #loading {
|
||||
cursor: n-resize;
|
||||
}
|
||||
|
||||
#chat:has(.has-files) #resize-bar {
|
||||
top: 46px;
|
||||
}
|
||||
|
||||
#attachments {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
@@ -1035,6 +1083,10 @@ label[for="reasoning-tokens"] {
|
||||
background-image: url(icons/screen.svg);
|
||||
}
|
||||
|
||||
#scrolling.not-following {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
#json {
|
||||
background-image: url(icons/json-off.svg);
|
||||
}
|
||||
@@ -1195,6 +1247,32 @@ label[for="reasoning-tokens"] {
|
||||
background: #89bb77;
|
||||
}
|
||||
|
||||
@keyframes swivel {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
50% {
|
||||
left: calc(100%);
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
|
||||
0%,
|
||||
75%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes wiggling {
|
||||
0% {
|
||||
transform: translate(0px);
|
||||
|
@@ -33,7 +33,7 @@
|
||||
<div id="title-text"></div>
|
||||
</div>
|
||||
|
||||
<div id="messages"></div>
|
||||
<div id="messages" tabindex="0"></div>
|
||||
|
||||
<div id="chat">
|
||||
<button id="top" class="hidden" title="Scroll to top"></button>
|
||||
|
@@ -1,7 +1,6 @@
|
||||
(() => {
|
||||
const $version = document.getElementById("version"),
|
||||
$total = document.getElementById("total"),
|
||||
$notifications = document.getElementById("notifications"),
|
||||
$title = document.getElementById("title"),
|
||||
$titleRefresh = document.getElementById("title-refresh"),
|
||||
$titleText = document.getElementById("title-text"),
|
||||
@@ -34,7 +33,8 @@
|
||||
$password = document.getElementById("password"),
|
||||
$login = document.getElementById("login");
|
||||
|
||||
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
||||
const nearBottom = 22,
|
||||
timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
||||
|
||||
let platform = "";
|
||||
|
||||
@@ -50,6 +50,8 @@
|
||||
promptList = [];
|
||||
|
||||
let autoScrolling = false,
|
||||
followTail = true,
|
||||
awaitingScroll = false,
|
||||
jsonMode = false,
|
||||
searchTool = false,
|
||||
chatTitle = false;
|
||||
@@ -66,34 +68,6 @@
|
||||
$total.textContent = formatMoney(totalCost);
|
||||
}
|
||||
|
||||
async function notify(msg, persistent = false) {
|
||||
console.warn(msg);
|
||||
|
||||
const notification = make("div", "notification", "off-screen");
|
||||
|
||||
notification.textContent = msg instanceof Error ? msg.message : msg;
|
||||
|
||||
$notifications.appendChild(notification);
|
||||
|
||||
await wait(250);
|
||||
|
||||
notification.classList.remove("off-screen");
|
||||
|
||||
if (persistent) {
|
||||
return;
|
||||
}
|
||||
|
||||
await wait(5000);
|
||||
|
||||
notification.style.height = `${notification.getBoundingClientRect().height}px`;
|
||||
|
||||
notification.classList.add("off-screen");
|
||||
|
||||
await wait(250);
|
||||
|
||||
notification.remove();
|
||||
}
|
||||
|
||||
function updateTitle() {
|
||||
const title = chatTitle || (messages.length ? "New Chat" : "");
|
||||
|
||||
@@ -106,26 +80,44 @@
|
||||
storeValue("title", chatTitle);
|
||||
}
|
||||
|
||||
function distanceFromBottom() {
|
||||
return $messages.scrollHeight - ($messages.scrollTop + $messages.clientHeight);
|
||||
}
|
||||
|
||||
function updateScrollButton() {
|
||||
const bottom = $messages.scrollHeight - ($messages.scrollTop + $messages.offsetHeight);
|
||||
const bottom = distanceFromBottom();
|
||||
|
||||
$top.classList.toggle("hidden", $messages.scrollTop < 80);
|
||||
$bottom.classList.toggle("hidden", bottom < 80);
|
||||
}
|
||||
|
||||
function setFollowTail(follow) {
|
||||
followTail = follow;
|
||||
|
||||
$scrolling.classList.toggle("not-following", !followTail);
|
||||
}
|
||||
|
||||
function scroll(force = false, instant = false) {
|
||||
if (!autoScrolling && !force) {
|
||||
if (awaitingScroll || !(followTail || force)) {
|
||||
updateScrollButton();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
awaitingScroll = true;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
awaitingScroll = false;
|
||||
|
||||
if (!followTail && !force) {
|
||||
return;
|
||||
}
|
||||
|
||||
$messages.scroll({
|
||||
top: $messages.scrollHeight,
|
||||
behavior: instant ? "instant" : "smooth",
|
||||
});
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
|
||||
function preloadIcons(icons) {
|
||||
@@ -215,6 +207,13 @@
|
||||
|
||||
this.#_message.appendChild(_body);
|
||||
|
||||
// loader
|
||||
const _loader = make("div", "loader");
|
||||
|
||||
_loader.innerHTML = "<span></span>".repeat(3);
|
||||
|
||||
_body.appendChild(_loader);
|
||||
|
||||
// message files
|
||||
this.#_files = make("div", "files");
|
||||
|
||||
@@ -233,12 +232,14 @@
|
||||
_reasoning.appendChild(_toggle);
|
||||
|
||||
_toggle.addEventListener("click", () => {
|
||||
_reasoning.classList.toggle("expanded");
|
||||
let delta = this.#updateReasoningHeight() + 16; // margin
|
||||
|
||||
if (_reasoning.classList.contains("expanded")) {
|
||||
this.#updateReasoningHeight();
|
||||
if (!_reasoning.classList.toggle("expanded")) {
|
||||
delta = -delta;
|
||||
}
|
||||
|
||||
setFollowTail(distanceFromBottom() + delta <= nearBottom);
|
||||
|
||||
updateScrollButton();
|
||||
});
|
||||
|
||||
@@ -282,7 +283,13 @@
|
||||
this.#_tool.appendChild(_call);
|
||||
|
||||
_call.addEventListener("click", () => {
|
||||
this.#_tool.classList.toggle("expanded");
|
||||
let delta = this.#updateToolHeight() + 16; // margin
|
||||
|
||||
if (!this.#_tool.classList.toggle("expanded")) {
|
||||
delta = -delta;
|
||||
}
|
||||
|
||||
setFollowTail(distanceFromBottom() + delta <= nearBottom);
|
||||
|
||||
updateScrollButton();
|
||||
});
|
||||
@@ -324,6 +331,10 @@
|
||||
_optCollapse.addEventListener("click", () => {
|
||||
this.#_message.classList.toggle("collapsed");
|
||||
|
||||
updateScrollButton();
|
||||
|
||||
setFollowTail(distanceFromBottom() <= nearBottom);
|
||||
|
||||
this.#save();
|
||||
});
|
||||
|
||||
@@ -432,13 +443,20 @@
|
||||
}
|
||||
|
||||
#updateReasoningHeight() {
|
||||
this.#_reasoning.parentNode.style.setProperty("--height", `${this.#_reasoning.scrollHeight}px`);
|
||||
const height = this.#_reasoning.scrollHeight;
|
||||
|
||||
this.#_reasoning.parentNode.style.setProperty("--height", `${height}px`);
|
||||
|
||||
return height;
|
||||
}
|
||||
|
||||
#updateToolHeight() {
|
||||
const result = this.#_tool.querySelector(".result");
|
||||
const result = this.#_tool.querySelector(".result"),
|
||||
height = result.scrollHeight;
|
||||
|
||||
this.#_tool.style.setProperty("--height", `${result.scrollHeight}px`);
|
||||
this.#_tool.style.setProperty("--height", `${height}px`);
|
||||
|
||||
return height;
|
||||
}
|
||||
|
||||
#morph(from, to) {
|
||||
@@ -524,8 +542,6 @@
|
||||
|
||||
this.#_message.classList.toggle("has-tool", !!this.#tool);
|
||||
|
||||
this.#updateToolHeight();
|
||||
|
||||
noScroll || scroll();
|
||||
|
||||
updateScrollButton();
|
||||
@@ -565,8 +581,6 @@
|
||||
|
||||
if (!only || only === "reasoning") {
|
||||
this.#patch("reasoning", this.#_reasoning, this.#reasoning, () => {
|
||||
this.#updateReasoningHeight();
|
||||
|
||||
noScroll || scroll();
|
||||
|
||||
updateScrollButton();
|
||||
@@ -829,6 +843,10 @@
|
||||
this.#render(false, true);
|
||||
this.#save();
|
||||
}
|
||||
|
||||
setFollowTail(distanceFromBottom() <= nearBottom);
|
||||
|
||||
updateScrollButton();
|
||||
}
|
||||
|
||||
delete() {
|
||||
@@ -951,6 +969,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (autoScrolling) {
|
||||
setFollowTail(true);
|
||||
}
|
||||
|
||||
let temperature = parseFloat($temperature.value);
|
||||
|
||||
if (Number.isNaN(temperature) || temperature < 0 || temperature > 2) {
|
||||
@@ -1308,7 +1330,9 @@
|
||||
}
|
||||
|
||||
if (message.tags) {
|
||||
message.tags.forEach(tag => obj.addTag(tag));
|
||||
message.tags.forEach(tag => {
|
||||
obj.addTag(tag);
|
||||
});
|
||||
}
|
||||
|
||||
if (message.tool) {
|
||||
@@ -1324,10 +1348,9 @@
|
||||
|
||||
updateTitle();
|
||||
|
||||
scroll();
|
||||
|
||||
// small fix, sometimes when hard reloading we don't scroll all the way
|
||||
setTimeout(scroll, 250);
|
||||
requestAnimationFrame(() => {
|
||||
$messages.scrollTop = $messages.scrollHeight;
|
||||
});
|
||||
}
|
||||
|
||||
let attachments = [];
|
||||
@@ -1437,7 +1460,17 @@
|
||||
updateScrollButton();
|
||||
});
|
||||
|
||||
$messages.addEventListener("wheel", event => {
|
||||
if (event.deltaY < 0) {
|
||||
setFollowTail(false);
|
||||
} else {
|
||||
setFollowTail(distanceFromBottom() - event.deltaY <= nearBottom);
|
||||
}
|
||||
});
|
||||
|
||||
$bottom.addEventListener("click", () => {
|
||||
setFollowTail(true);
|
||||
|
||||
$messages.scroll({
|
||||
top: $messages.scrollHeight,
|
||||
behavior: "smooth",
|
||||
@@ -1445,6 +1478,8 @@
|
||||
});
|
||||
|
||||
$top.addEventListener("click", () => {
|
||||
setFollowTail($messages.scrollHeight <= $messages.clientHeight);
|
||||
|
||||
$messages.scroll({
|
||||
top: 0,
|
||||
behavior: "smooth",
|
||||
@@ -1452,7 +1487,7 @@
|
||||
});
|
||||
|
||||
$resizeBar.addEventListener("mousedown", event => {
|
||||
const isAtBottom = $messages.scrollHeight - ($messages.scrollTop + $messages.offsetHeight) <= 10;
|
||||
const isAtBottom = $messages.scrollHeight - ($messages.scrollTop + $messages.clientHeight) <= 10;
|
||||
|
||||
if (event.button === 1) {
|
||||
$chat.style.height = "";
|
||||
@@ -1671,6 +1706,8 @@
|
||||
autoScrolling = !autoScrolling;
|
||||
|
||||
if (autoScrolling) {
|
||||
setFollowTail(true);
|
||||
|
||||
$scrolling.title = "Turn off auto-scrolling";
|
||||
$scrolling.classList.add("on");
|
||||
|
||||
@@ -1729,7 +1766,7 @@
|
||||
}
|
||||
|
||||
const total = window.innerHeight,
|
||||
height = clamp(window.innerHeight - event.clientY, 100, total - 240);
|
||||
height = clamp(window.innerHeight - event.clientY + (attachments.length ? 50 : 0), 100, total - 240);
|
||||
|
||||
$chat.style.height = `${height}px`;
|
||||
|
||||
@@ -1744,6 +1781,52 @@
|
||||
document.body.classList.remove("resizing");
|
||||
});
|
||||
|
||||
addEventListener("keydown", event => {
|
||||
if (["TEXTAREA", "INPUT", "SELECT"].includes(document.activeElement?.tagName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let delta;
|
||||
|
||||
switch (event.key) {
|
||||
case "PageUp":
|
||||
case "ArrowUp":
|
||||
delta = event.key === "PageUp" ? -$messages.clientHeight : -120;
|
||||
|
||||
setFollowTail(false);
|
||||
|
||||
break;
|
||||
case "PageDown":
|
||||
case "ArrowDown":
|
||||
delta = event.key === "PageDown" ? $messages.clientHeight : 120;
|
||||
|
||||
setFollowTail(distanceFromBottom() - delta <= nearBottom);
|
||||
|
||||
break;
|
||||
case "Home":
|
||||
delta = -$messages.scrollTop;
|
||||
|
||||
setFollowTail(false);
|
||||
|
||||
break;
|
||||
case "End":
|
||||
delta = $messages.scrollHeight - $messages.clientHeight - $messages.scrollTop;
|
||||
|
||||
setFollowTail(true);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (delta) {
|
||||
event.preventDefault();
|
||||
|
||||
$messages.scrollBy({
|
||||
top: delta,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
dropdown($role);
|
||||
dropdown($reasoningEffort);
|
||||
|
||||
|
@@ -329,3 +329,35 @@ async function detectPlatform() {
|
||||
|
||||
return `${os || "Unknown OS"}${arch ? `, ${arch}` : ""}`;
|
||||
}
|
||||
|
||||
(() => {
|
||||
const $notifications = document.getElementById("notifications");
|
||||
|
||||
window.notify = async (msg, persistent = false) => {
|
||||
console.warn(msg);
|
||||
|
||||
const notification = make("div", "notification", "off-screen");
|
||||
|
||||
notification.textContent = msg instanceof Error ? msg.message : msg;
|
||||
|
||||
$notifications.appendChild(notification);
|
||||
|
||||
await wait(250);
|
||||
|
||||
notification.classList.remove("off-screen");
|
||||
|
||||
if (persistent) {
|
||||
return;
|
||||
}
|
||||
|
||||
await wait(5000);
|
||||
|
||||
notification.style.height = `${notification.getBoundingClientRect().height}px`;
|
||||
|
||||
notification.classList.add("off-screen");
|
||||
|
||||
await wait(250);
|
||||
|
||||
notification.remove();
|
||||
};
|
||||
})();
|
||||
|
Reference in New Issue
Block a user