make some overall QoL improvements
This commit is contained in:
+98
-28
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user