make some overall QoL improvements

This commit is contained in:
Frederik Spirgi
2026-09-30 10:06:29 +02:00
parent 7a1b152f20
commit d15f7867d5
9 changed files with 239 additions and 45 deletions
+98 -28
View File
@@ -4,7 +4,7 @@
import { del, get, post, put, stream } from "./api.js?v=20260909-account-controls";
import { regenerateInline } from "./regeneration.js";
import { appendSources, createWebSearchToggle } from "./webSearch.js";
import { appendAttachments, createAttachmentComposer } from "./attachments.js?v=20260923-vision-provenance";
import { appendAttachments, createAttachmentComposer } from "./attachments.js?v=20260930-chat-usability";
import { createReasoningToggle, finishReasoning, updateReasoning } from "./reasoning.js?v=20260921-reasoning-capability";
import { closeChatDrawer, openChatDrawer } from "./chatDrawer.js?v=20260909-account-controls";
import { onboardingPrompts, requestedChatId } from "./constants.js?v=20260909-account-controls";
@@ -131,6 +131,12 @@ let chatSearchResults = null;
let chatSearchError = "";
let chatSearchTimer = null;
let chatSearchRequest = 0;
let chatOpenRequest = 0;
const chatDrafts = new Map();
const jumpToLatest = document.getElementById("jumpToLatest");
let scrollFrame = null;
let renderedChatId = null;
let renderedMessages = new Map();
function setNotice(message = "", state = "") {
@@ -412,11 +418,22 @@ function resizeComposerInput() {
}
function scrollMessagesToBottom() {
requestAnimationFrame(() => {
if (scrollFrame !== null) cancelAnimationFrame(scrollFrame);
scrollFrame = requestAnimationFrame(() => {
scrollFrame = null;
messagesDiv.scrollTop = messagesDiv.scrollHeight;
updateJumpToLatest();
});
}
function updateJumpToLatest() {
const distance = messagesDiv.scrollHeight - messagesDiv.scrollTop - messagesDiv.clientHeight;
jumpToLatest.classList.toggle("hidden", distance < 180);
}
messagesDiv.addEventListener("scroll", updateJumpToLatest, {passive: true});
jumpToLatest.addEventListener("click", scrollMessagesToBottom);
function getNewestMessage() {
return currentMessages.length ? currentMessages[currentMessages.length - 1] : null;
}
@@ -863,21 +880,46 @@ function addMessage(contentOrMessage, isUser = false, isLoading = false, options
}
function renderMessages() {
messagesDiv.innerHTML = "";
if (scrollFrame !== null) { cancelAnimationFrame(scrollFrame); scrollFrame = null; }
const sameChat = renderedChatId === activeChatId;
const wasReadingEarlier = sameChat && messagesDiv.scrollHeight - messagesDiv.scrollTop - messagesDiv.clientHeight >= 100;
const previousScrollTop = messagesDiv.scrollTop;
const chat = getChatById(activeChatId);
if (!currentMessages.length && (!chat || chat.assistant_persona_id)) {
messagesDiv.replaceChildren();
renderMessageEmptyState();
renderedMessages = new Map();
} else {
currentMessages.forEach((msg, index) => addMessage(msg, msg.role === "user", false, {
messageIndex: index,
isNewest: index === currentMessages.length - 1
}));
const fragment = document.createDocumentFragment();
const nextRendered = new Map();
currentMessages.forEach((msg, index) => {
const key = msg.id ? String(msg.id) : null;
const snapshot = JSON.stringify(msg);
const previous = sameChat && key ? renderedMessages.get(key) : null;
const card = previous?.snapshot === snapshot && previous.node.isConnected
? previous.node
: addMessage(msg, msg.role === "user", false, {
messageIndex: index,
isNewest: index === currentMessages.length - 1,
detached: true
});
fragment.append(card);
if (key) nextRendered.set(key, {snapshot, node: card});
});
messagesDiv.replaceChildren(fragment);
renderedMessages = nextRendered;
}
renderedChatId = activeChatId;
renderDrawerSummary();
updateChatActionState();
updateComposerPlaceholder();
if (currentMessages.length) {
if (wasReadingEarlier) {
messagesDiv.scrollTop = previousScrollTop;
updateJumpToLatest();
} else if (currentMessages.length) {
scrollMessagesToBottom();
} else {
updateJumpToLatest();
}
}
@@ -1127,7 +1169,7 @@ function renderChatList() {
return;
}
const folderGroups = filtered
.filter((chat) => chat.folder_name && !chat.archived_at)
.filter((chat) => chat.folder_name && !chat.is_pinned && !chat.archived_at)
.reduce((map, chat) => {
const key = String(chat.folder_name).trim();
const items = map.get(key) || [];
@@ -1152,13 +1194,13 @@ function renderChatList() {
list.setAttribute("role", "list");
section.append(heading, list);
group.items.forEach((chat) => {
const item = document.createElement("div"), content = document.createElement("div"), title = document.createElement("span"), meta = document.createElement("small"), remove = document.createElement("button");
const item = document.createElement("div"), content = document.createElement("button"), title = document.createElement("span"), meta = document.createElement("small"), remove = document.createElement("button");
item.className = `chat-item${chat.id === activeChatId ? " active" : ""}`;
item.setAttribute("role", "button");
item.tabIndex = 0;
item.setAttribute("aria-label", `Open chat ${chat.title}`);
if (chat.id === activeChatId) item.setAttribute("aria-current", "page");
content.className = "chat-item-copy";
item.setAttribute("role", "listitem");
content.type = "button";
content.className = "chat-item-copy secondary-action";
content.setAttribute("aria-label", `Open chat ${chat.title}`);
if (chat.id === activeChatId) content.setAttribute("aria-current", "page");
title.textContent = chat.title;
meta.className = "chat-item-meta";
const chatType = chat.assistant_persona_id
@@ -1175,16 +1217,19 @@ function renderChatList() {
await deleteChat(chat.id);
});
content.append(title, meta);
if (query && chat.matched_content) {
const excerpt = document.createElement("small");
const normalized = chat.matched_content.replace(/\s+/g, " ");
const matchAt = normalized.toLowerCase().indexOf(query);
const start = Math.max(0, matchAt - 35);
excerpt.className = "chat-item-excerpt";
excerpt.textContent = `${start ? "…" : ""}${normalized.slice(start, start + 110)}${normalized.length > start + 110 ? "…" : ""}`;
content.append(excerpt);
}
item.append(content, remove);
item.addEventListener("click", () => {
content.addEventListener("click", () => {
closeMobileSidebar();
setActiveChat(chat.id);
});
item.addEventListener("keydown", (event) => {
if (event.key !== "Enter" && event.key !== " ") return;
event.preventDefault();
closeMobileSidebar();
void setActiveChat(chat.id);
void setActiveChat(chat.id, {matchMessageId: query ? chat.matched_message_id : null});
});
list.appendChild(item);
});
@@ -1500,8 +1545,21 @@ async function stopGeneration() {
setNotice(res.stopped ? "Stopping generation..." : "Generation already finished.");
}
async function setActiveChat(id, {preserveComposer = false} = {}) {
if (!preserveComposer && id !== activeChatId) attachmentComposer.discard();
async function setActiveChat(id, {preserveComposer = false, matchMessageId = null} = {}) {
const openRequest = ++chatOpenRequest;
if (!preserveComposer && id !== activeChatId) {
if (activeChatId != null) {
chatDrafts.set(activeChatId, msgInput.value);
attachmentComposer.saveDraft(activeChatId);
msgInput.value = chatDrafts.get(id) || "";
attachmentComposer.restoreDraft(id);
resizeComposerInput();
} else if (!msgInput.value && !attachmentComposer.pending && !attachmentComposer.attachments.length) {
msgInput.value = chatDrafts.get(id) || "";
attachmentComposer.restoreDraft(id);
resizeComposerInput();
}
}
if (isMobileLayout()) closeMobileSidebar();
setChatLoading(true, "Opening chat...");
setChatActivity(true, {
@@ -1513,16 +1571,27 @@ async function setActiveChat(id, {preserveComposer = false} = {}) {
history.replaceState(null, "", `/app/chats/${id}`);
messageRetryState = new Map();
updateWorkspaceCopy(); renderChatList(); updateChatActionState();
const res = await get(`/chats/${id}/messages`);
let res = await get(`/chats/${id}/messages`);
if (openRequest !== chatOpenRequest) { setChatLoading(false); setChatActivity(false); return; }
if (res.error) {
setChatLoading(false);
setChatActivity(false);
return setNotice(res.error, "error");
}
if (matchMessageId && !res.messages?.some(message => message.id === matchMessageId)) {
const activated = await post(`/chats/${id}/branches/${matchMessageId}/activate`, {includeDescendants: true});
if (!activated.error) res = await get(`/chats/${id}/messages`);
if (openRequest !== chatOpenRequest) { setChatLoading(false); setChatActivity(false); return; }
}
const existing = getChatById(id);
if (res.chat && existing) Object.assign(existing, res.chat);
populateGenerationSettings(res.chat || existing);
currentMessages = res.messages || []; renderMessages();
if (matchMessageId) requestAnimationFrame(() => {
const target = [...messagesDiv.querySelectorAll("[data-message-id]")]
.find(node => Number(node.dataset.messageId) === Number(matchMessageId));
if (target) { target.scrollIntoView({block: "center"}); updateJumpToLatest(); }
});
updateSendState();
setChatLoading(false);
setChatActivity(false);
@@ -1585,7 +1654,6 @@ function scheduleChatSearch() {
}
async function createNewChat({preserveComposer = false} = {}) {
if (!preserveComposer) attachmentComposer.discard();
setChatLoading(true, "Creating chat...");
setChatActivity(true, {
eyebrow: "Creating chat",
@@ -1905,7 +1973,9 @@ async function deleteChat(id) {
return setNotice(res.error, "error");
}
chatSessions = chatSessions.filter((item) => item.id !== id);
if (activeChatId === id) { activeChatId = null; currentMessages = []; renderMessages(); }
chatDrafts.delete(id);
attachmentComposer.discardDraft(id);
if (activeChatId === id) { attachmentComposer.discard(); msgInput.value = ""; activeChatId = null; currentMessages = []; renderMessages(); }
messageRetryState = new Map();
await loadSummary(); renderChatList();
if (chatSessions.length) await setActiveChat(chatSessions[0].id); else await createNewChat();