make some overall QoL improvements
This commit is contained in:
@@ -41,6 +41,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
<button id="chatSidebarBackdrop" class="chat-sidebar-backdrop" type="button"
|
||||||
|
aria-label="Close conversation history" hidden></button>
|
||||||
|
|
||||||
<section id="main" class="chat-main chat-main-focus" aria-label="Active conversation">
|
<section id="main" class="chat-main chat-main-focus" aria-label="Active conversation">
|
||||||
<div class="chat-heading">
|
<div class="chat-heading">
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ body.site-page { width:100%; max-width:none; margin:0; padding:0; background:var
|
|||||||
.chat-page .chat-card { height:calc(100dvh - var(--shell-header-height)); min-height:0; border:0; border-radius:0; box-shadow:none; }
|
.chat-page .chat-card { height:calc(100dvh - var(--shell-header-height)); min-height:0; border:0; border-radius:0; box-shadow:none; }
|
||||||
.chat-page .chat-layout { grid-template-columns:260px minmax(0,1fr); }
|
.chat-page .chat-layout { grid-template-columns:260px minmax(0,1fr); }
|
||||||
.chat-page .chat-sidebar { position:static; inset:auto; width:auto; transform:none; box-shadow:none; grid-template-rows:auto minmax(0,1fr); padding:24px 18px; gap:20px; }
|
.chat-page .chat-sidebar { position:static; inset:auto; width:auto; transform:none; box-shadow:none; grid-template-rows:auto minmax(0,1fr); padding:24px 18px; gap:20px; }
|
||||||
|
.chat-page .chat-sidebar-backdrop { display:none; }
|
||||||
.chat-page .sidebar-workspace-card { gap:16px; border:0; padding:0; }
|
.chat-page .sidebar-workspace-card { gap:16px; border:0; padding:0; }
|
||||||
.chat-page .sidebar-workspace-card h2 { margin:0; font-size:.85rem; letter-spacing:0; font-family:var(--font-body); color:var(--text-soft); }
|
.chat-page .sidebar-workspace-card h2 { margin:0; font-size:.85rem; letter-spacing:0; font-family:var(--font-body); color:var(--text-soft); }
|
||||||
.chat-page .chat-main { grid-template-rows:auto minmax(0,1fr) auto; }
|
.chat-page .chat-main { grid-template-rows:auto minmax(0,1fr) auto; }
|
||||||
@@ -44,6 +45,7 @@ body.site-page { width:100%; max-width:none; margin:0; padding:0; background:var
|
|||||||
.chat-page .chat-layout { grid-template-columns:1fr; }
|
.chat-page .chat-layout { grid-template-columns:1fr; }
|
||||||
.chat-page .chat-sidebar { position:fixed; inset:var(--shell-header-height) auto 0 0; width:min(320px,88vw); z-index:80; transform:translateX(-104%); box-shadow:var(--shadow-lg); }
|
.chat-page .chat-sidebar { position:fixed; inset:var(--shell-header-height) auto 0 0; width:min(320px,88vw); z-index:80; transform:translateX(-104%); box-shadow:var(--shadow-lg); }
|
||||||
.chat-page.sidebar-open .chat-sidebar { transform:translateX(0); }
|
.chat-page.sidebar-open .chat-sidebar { transform:translateX(0); }
|
||||||
|
.chat-page .chat-sidebar-backdrop:not([hidden]) { display:block; position:fixed; inset:var(--shell-header-height) 0 0; z-index:79; width:100%; height:auto; padding:0; border:0; border-radius:0; background:rgba(0,0,0,.45); }
|
||||||
.chat-page .sidebar-toggle { display:inline-flex; width:auto; min-width:0; padding:8px 12px; font-size:.8rem; border-radius:10px; }
|
.chat-page .sidebar-toggle { display:inline-flex; width:auto; min-width:0; padding:8px 12px; font-size:.8rem; border-radius:10px; }
|
||||||
.site-page .settings-layout { grid-template-columns:1fr; }
|
.site-page .settings-layout { grid-template-columns:1fr; }
|
||||||
.site-page .settings-nav { display:flex; flex-wrap:wrap; }
|
.site-page .settings-nav { display:flex; flex-wrap:wrap; }
|
||||||
|
|||||||
@@ -419,6 +419,14 @@ input:focus, textarea:focus, select:focus { border-color: var(--brand); }
|
|||||||
.msg.user .msg-actions { right: 0; left: auto; justify-content: flex-end; }
|
.msg.user .msg-actions { right: 0; left: auto; justify-content: flex-end; }
|
||||||
.msg:hover .msg-actions, .msg:focus-within .msg-actions { opacity: 1; }
|
.msg:hover .msg-actions, .msg:focus-within .msg-actions { opacity: 1; }
|
||||||
.msg-action-btn { min-height: 30px; padding: 5px 8px; color: var(--text-soft) !important; background: transparent !important; border: 1px solid var(--border); border-radius: 8px; font-size: .7rem; }
|
.msg-action-btn { min-height: 30px; padding: 5px 8px; color: var(--text-soft) !important; background: transparent !important; border: 1px solid var(--border); border-radius: 8px; font-size: .7rem; }
|
||||||
|
.msg-action-menu { position: relative; }
|
||||||
|
.msg-action-menu > summary { display: flex; align-items: center; cursor: pointer; list-style: none; }
|
||||||
|
.msg-action-menu > summary::-webkit-details-marker { display: none; }
|
||||||
|
.msg-action-menu-items { position: absolute; bottom: calc(100% + 4px); left: 0; min-width: 130px; padding: 5px; display: grid; gap: 3px; background: var(--surface-raised); border: 1px solid var(--border); border-radius: 9px; box-shadow: var(--shadow-lg); }
|
||||||
|
.msg.user .msg-action-menu-items { left: auto; right: 0; }
|
||||||
|
.msg-action-menu-items .msg-action-btn { width: 100%; text-align: left; border: 0; }
|
||||||
|
.msg-action-menu-items .msg-action-btn:hover, .msg-action-menu-items .msg-action-btn:focus-visible { background: var(--surface-hover) !important; }
|
||||||
|
.msg-action-menu[open] { z-index: 5; }
|
||||||
.msg-variant-index { align-self: center; color: var(--text-muted); font-size: .7rem; }
|
.msg-variant-index { align-self: center; color: var(--text-muted); font-size: .7rem; }
|
||||||
.typing { display: flex; gap: 5px; }
|
.typing { display: flex; gap: 5px; }
|
||||||
.typing-dot { width: 6px; height: 6px; background: var(--brand); border-radius: 50%; animation: typing 1.2s infinite; }
|
.typing-dot { width: 6px; height: 6px; background: var(--brand); border-radius: 50%; animation: typing 1.2s infinite; }
|
||||||
|
|||||||
+181
-40
@@ -127,6 +127,10 @@ let chatLoadingDepth = 0;
|
|||||||
let chatActivityDepth = 0;
|
let chatActivityDepth = 0;
|
||||||
let messageRetryState = new Map();
|
let messageRetryState = new Map();
|
||||||
let availableModelsById = new Map();
|
let availableModelsById = new Map();
|
||||||
|
let chatSearchResults = null;
|
||||||
|
let chatSearchError = "";
|
||||||
|
let chatSearchTimer = null;
|
||||||
|
let chatSearchRequest = 0;
|
||||||
|
|
||||||
|
|
||||||
function setNotice(message = "", state = "") {
|
function setNotice(message = "", state = "") {
|
||||||
@@ -193,15 +197,33 @@ function isMobileLayout() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function closeMobileSidebar() {
|
function closeMobileSidebar() {
|
||||||
if (!chatSidebar) return;
|
if (!chatSidebar || !document.body.classList.contains("sidebar-open")) return;
|
||||||
document.body.classList.remove("sidebar-open");
|
document.body.classList.remove("sidebar-open");
|
||||||
if (chatSidebarToggle) chatSidebarToggle.setAttribute("aria-expanded", "false");
|
if (chatSidebarToggle) {
|
||||||
|
chatSidebarToggle.setAttribute("aria-expanded", "false");
|
||||||
|
chatSidebarToggle.setAttribute("aria-label", "Open conversation history");
|
||||||
|
}
|
||||||
|
chatSidebar.inert = isMobileLayout();
|
||||||
|
chatSidebar.removeAttribute("aria-modal");
|
||||||
|
chatSidebar.removeAttribute("role");
|
||||||
|
const backdrop = document.getElementById("chatSidebarBackdrop");
|
||||||
|
if (backdrop) backdrop.hidden = true;
|
||||||
|
chatSidebarToggle?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function openMobileSidebar() {
|
function openMobileSidebar() {
|
||||||
if (!chatSidebar) return;
|
if (!chatSidebar || !isMobileLayout()) return;
|
||||||
|
chatSidebar.inert = false;
|
||||||
|
chatSidebar.setAttribute("role", "dialog");
|
||||||
|
chatSidebar.setAttribute("aria-modal", "true");
|
||||||
document.body.classList.add("sidebar-open");
|
document.body.classList.add("sidebar-open");
|
||||||
if (chatSidebarToggle) chatSidebarToggle.setAttribute("aria-expanded", "true");
|
if (chatSidebarToggle) {
|
||||||
|
chatSidebarToggle.setAttribute("aria-expanded", "true");
|
||||||
|
chatSidebarToggle.setAttribute("aria-label", "Close conversation history");
|
||||||
|
}
|
||||||
|
const backdrop = document.getElementById("chatSidebarBackdrop");
|
||||||
|
if (backdrop) backdrop.hidden = false;
|
||||||
|
chatSearchInput?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasCompletedOnboarding() {
|
function hasCompletedOnboarding() {
|
||||||
@@ -477,7 +499,10 @@ function ensureRetryState(message) {
|
|||||||
|
|
||||||
async function openCreatedChat(chat) {
|
async function openCreatedChat(chat) {
|
||||||
if (!chat?.id) throw new Error("The server did not return the newly created chat");
|
if (!chat?.id) throw new Error("The server did not return the newly created chat");
|
||||||
if (chatSearchInput) chatSearchInput.value = "";
|
if (chatSearchInput) {
|
||||||
|
chatSearchInput.value = "";
|
||||||
|
scheduleChatSearch();
|
||||||
|
}
|
||||||
const chatsResponse = await get("/chats");
|
const chatsResponse = await get("/chats");
|
||||||
if (chatsResponse.error) throw new Error(chatsResponse.error);
|
if (chatsResponse.error) throw new Error(chatsResponse.error);
|
||||||
chatSessions = chatsResponse.chats || [];
|
chatSessions = chatsResponse.chats || [];
|
||||||
@@ -705,6 +730,44 @@ function addMessage(contentOrMessage, isUser = false, isLoading = false, options
|
|||||||
header.textContent = isMessageUser ? "You" : (activeChat?.assistant_persona_name || "Assistant");
|
header.textContent = isMessageUser ? "You" : (activeChat?.assistant_persona_name || "Assistant");
|
||||||
setMessageContent(body, message.content);
|
setMessageContent(body, message.content);
|
||||||
|
|
||||||
|
const copyBtn = document.createElement("button");
|
||||||
|
copyBtn.type = "button";
|
||||||
|
copyBtn.className = "msg-action-btn";
|
||||||
|
copyBtn.textContent = "Copy";
|
||||||
|
copyBtn.setAttribute("aria-label", `Copy ${isMessageUser ? "your" : "assistant"} message`);
|
||||||
|
copyBtn.addEventListener("click", async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(message.content || "");
|
||||||
|
setNotice("Message copied.", "success");
|
||||||
|
} catch {
|
||||||
|
setNotice("Unable to copy message.", "error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
actions.appendChild(copyBtn);
|
||||||
|
|
||||||
|
if (message.deliveryStatus === "failed" && isMessageUser) {
|
||||||
|
const retryFailedBtn = document.createElement("button");
|
||||||
|
retryFailedBtn.type = "button";
|
||||||
|
retryFailedBtn.className = "msg-action-btn";
|
||||||
|
retryFailedBtn.dataset.messageAction = "retry-failed";
|
||||||
|
retryFailedBtn.textContent = "Retry";
|
||||||
|
retryFailedBtn.disabled = isProcessing;
|
||||||
|
retryFailedBtn.addEventListener("click", () => { void sendMessage({ retryMessage: message }); });
|
||||||
|
actions.appendChild(retryFailedBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
const menu = document.createElement("details");
|
||||||
|
menu.className = "msg-action-menu";
|
||||||
|
const menuTrigger = document.createElement("summary");
|
||||||
|
menuTrigger.className = "msg-action-btn";
|
||||||
|
menuTrigger.textContent = "More";
|
||||||
|
menuTrigger.setAttribute("aria-label", "More message actions");
|
||||||
|
const menuItems = document.createElement("div");
|
||||||
|
menuItems.className = "msg-action-menu-items";
|
||||||
|
menuItems.addEventListener("click", () => { menu.open = false; });
|
||||||
|
menu.append(menuTrigger, menuItems);
|
||||||
|
if (message.id) actions.appendChild(menu);
|
||||||
|
|
||||||
const editBtn = document.createElement("button");
|
const editBtn = document.createElement("button");
|
||||||
editBtn.type = "button";
|
editBtn.type = "button";
|
||||||
editBtn.className = "msg-action-btn";
|
editBtn.className = "msg-action-btn";
|
||||||
@@ -713,7 +776,7 @@ function addMessage(contentOrMessage, isUser = false, isLoading = false, options
|
|||||||
editBtn.addEventListener("click", () => {
|
editBtn.addEventListener("click", () => {
|
||||||
void editChatMessage(message.id, options.messageIndex ?? -1);
|
void editChatMessage(message.id, options.messageIndex ?? -1);
|
||||||
});
|
});
|
||||||
actions.appendChild(editBtn);
|
menuItems.appendChild(editBtn);
|
||||||
|
|
||||||
const deleteBtn = document.createElement("button");
|
const deleteBtn = document.createElement("button");
|
||||||
deleteBtn.type = "button";
|
deleteBtn.type = "button";
|
||||||
@@ -723,14 +786,14 @@ function addMessage(contentOrMessage, isUser = false, isLoading = false, options
|
|||||||
deleteBtn.addEventListener("click", () => {
|
deleteBtn.addEventListener("click", () => {
|
||||||
void deleteChatMessage(message.id, options.messageIndex ?? -1);
|
void deleteChatMessage(message.id, options.messageIndex ?? -1);
|
||||||
});
|
});
|
||||||
actions.appendChild(deleteBtn);
|
menuItems.appendChild(deleteBtn);
|
||||||
|
|
||||||
const branchBtn = document.createElement("button");
|
const branchBtn = document.createElement("button");
|
||||||
branchBtn.type = "button";
|
branchBtn.type = "button";
|
||||||
branchBtn.className = "msg-action-btn";
|
branchBtn.className = "msg-action-btn";
|
||||||
branchBtn.textContent = "Branch here";
|
branchBtn.textContent = "Branch here";
|
||||||
branchBtn.addEventListener("click", () => { void branchFromMessage(message.id); });
|
branchBtn.addEventListener("click", () => { void branchFromMessage(message.id); });
|
||||||
actions.appendChild(branchBtn);
|
menuItems.appendChild(branchBtn);
|
||||||
|
|
||||||
if (message.role === "bot") {
|
if (message.role === "bot") {
|
||||||
const retriesUsed = Math.max(0, Number(message.versionCount || 1) - 1);
|
const retriesUsed = Math.max(0, Number(message.versionCount || 1) - 1);
|
||||||
@@ -784,7 +847,7 @@ function addMessage(contentOrMessage, isUser = false, isLoading = false, options
|
|||||||
if (!isMessageUser) appendSources(msgDiv, message.sources);
|
if (!isMessageUser) appendSources(msgDiv, message.sources);
|
||||||
if (message.deliveryStatus === "failed" && message.errorMessage) {
|
if (message.deliveryStatus === "failed" && message.errorMessage) {
|
||||||
const error = document.createElement("p");
|
const error = document.createElement("p");
|
||||||
error.className = "status";
|
error.className = "status error";
|
||||||
error.textContent = message.errorMessage;
|
error.textContent = message.errorMessage;
|
||||||
msgDiv.appendChild(error);
|
msgDiv.appendChild(error);
|
||||||
}
|
}
|
||||||
@@ -1049,14 +1112,17 @@ function maybeShowPersonaNudge() {
|
|||||||
|
|
||||||
function renderChatList() {
|
function renderChatList() {
|
||||||
const query = chatSearchInput.value.trim().toLowerCase();
|
const query = chatSearchInput.value.trim().toLowerCase();
|
||||||
const filtered = chatSessions.filter((chat) => !query
|
const source = query ? (chatSearchResults ?? []) : chatSessions;
|
||||||
|
const filtered = source.filter((chat) => !query
|
||||||
|| chat.title.toLowerCase().includes(query)
|
|| chat.title.toLowerCase().includes(query)
|
||||||
|| String(chat.matched_content || "").toLowerCase().includes(query));
|
|| String(chat.matched_content || "").toLowerCase().includes(query));
|
||||||
chatList.innerHTML = "";
|
chatList.innerHTML = "";
|
||||||
if (!filtered.length) {
|
if (!filtered.length) {
|
||||||
const empty = document.createElement("p");
|
const empty = document.createElement("p");
|
||||||
empty.className = "status";
|
empty.className = "status";
|
||||||
empty.textContent = query ? "No chats match this search." : "No chats yet. Start a new one.";
|
empty.textContent = query && chatSearchError ? chatSearchError
|
||||||
|
: query && chatSearchResults === null ? "Searching chats..."
|
||||||
|
: query ? "No chats match this search." : "No chats yet. Start a new one.";
|
||||||
chatList.appendChild(empty);
|
chatList.appendChild(empty);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1105,6 +1171,7 @@ function renderChatList() {
|
|||||||
remove.setAttribute("aria-label", `Delete chat ${chat.title}`);
|
remove.setAttribute("aria-label", `Delete chat ${chat.title}`);
|
||||||
remove.addEventListener("click", async (event) => {
|
remove.addEventListener("click", async (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
closeMobileSidebar();
|
||||||
await deleteChat(chat.id);
|
await deleteChat(chat.id);
|
||||||
});
|
});
|
||||||
content.append(title, meta);
|
content.append(title, meta);
|
||||||
@@ -1408,6 +1475,9 @@ function setLoadingState(loading, overlayOptions = null) {
|
|||||||
messagesDiv.querySelectorAll('[data-message-action="regenerate"]').forEach((button) => {
|
messagesDiv.querySelectorAll('[data-message-action="regenerate"]').forEach((button) => {
|
||||||
button.disabled = loading || button.dataset.retryLimitReached === "true";
|
button.disabled = loading || button.dataset.retryLimitReached === "true";
|
||||||
});
|
});
|
||||||
|
messagesDiv.querySelectorAll('[data-message-action="retry-failed"]').forEach((button) => {
|
||||||
|
button.disabled = loading;
|
||||||
|
});
|
||||||
if (loading) {
|
if (loading) {
|
||||||
msgInput.placeholder = "Processing response...";
|
msgInput.placeholder = "Processing response...";
|
||||||
if (overlayOptions === false) {
|
if (overlayOptions === false) {
|
||||||
@@ -1460,13 +1530,17 @@ async function setActiveChat(id, {preserveComposer = false} = {}) {
|
|||||||
|
|
||||||
async function loadChatSessions() {
|
async function loadChatSessions() {
|
||||||
setChatLoading(true, "Loading chats...");
|
setChatLoading(true, "Loading chats...");
|
||||||
const query = chatSearchInput?.value.trim();
|
const res = await get("/chats");
|
||||||
const res = query ? await get(`/chats/search?q=${encodeURIComponent(query)}`) : await get("/chats");
|
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
setChatLoading(false);
|
setChatLoading(false);
|
||||||
return setNotice(res.error, "error");
|
return setNotice(res.error, "error");
|
||||||
}
|
}
|
||||||
chatSessions = res.chats || []; renderChatList();
|
chatSessions = res.chats || []; renderChatList();
|
||||||
|
if (activeChatId && getChatById(activeChatId)) {
|
||||||
|
updateWorkspaceCopy();
|
||||||
|
setChatLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (location.pathname.endsWith("/new") && !activeChatId) { currentMessages = []; renderMessages(); updateWorkspaceCopy(); updateSendState(); setChatLoading(false); return; }
|
if (location.pathname.endsWith("/new") && !activeChatId) { currentMessages = []; renderMessages(); updateWorkspaceCopy(); updateSendState(); setChatLoading(false); return; }
|
||||||
if (requestedChatId && !getChatById(requestedChatId) && !activeChatId) { setChatLoading(false); return setNotice("Conversation not found. Choose a chat from the sidebar or start a new one.", "error"); }
|
if (requestedChatId && !getChatById(requestedChatId) && !activeChatId) { setChatLoading(false); return setNotice("Conversation not found. Choose a chat from the sidebar or start a new one.", "error"); }
|
||||||
if (chatSessions.length) {
|
if (chatSessions.length) {
|
||||||
@@ -1483,6 +1557,33 @@ async function loadChatSessions() {
|
|||||||
return setNotice("Pick a starting action to begin.", "success");
|
return setNotice("Pick a starting action to begin.", "success");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function searchChatSessions(query, requestId) {
|
||||||
|
const res = await get(`/chats/search?q=${encodeURIComponent(query)}`);
|
||||||
|
if (requestId !== chatSearchRequest || chatSearchInput.value.trim() !== query) return;
|
||||||
|
if (res.error) {
|
||||||
|
chatSearchError = "Unable to search chats.";
|
||||||
|
renderChatList();
|
||||||
|
setNotice(res.error, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chatSearchResults = res.chats || [];
|
||||||
|
renderChatList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleChatSearch() {
|
||||||
|
clearTimeout(chatSearchTimer);
|
||||||
|
const requestId = ++chatSearchRequest;
|
||||||
|
const query = chatSearchInput.value.trim();
|
||||||
|
chatSearchResults = null;
|
||||||
|
chatSearchError = "";
|
||||||
|
if (!query) {
|
||||||
|
renderChatList();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
renderChatList();
|
||||||
|
chatSearchTimer = setTimeout(() => { void searchChatSessions(query, requestId); }, 300);
|
||||||
|
}
|
||||||
|
|
||||||
async function createNewChat({preserveComposer = false} = {}) {
|
async function createNewChat({preserveComposer = false} = {}) {
|
||||||
if (!preserveComposer) attachmentComposer.discard();
|
if (!preserveComposer) attachmentComposer.discard();
|
||||||
setChatLoading(true, "Creating chat...");
|
setChatLoading(true, "Creating chat...");
|
||||||
@@ -1852,12 +1953,16 @@ async function checkSession() {
|
|||||||
msgInput.focus(); return true;
|
msgInput.focus(); return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendMessage() {
|
async function sendMessage({ retryMessage = null } = {}) {
|
||||||
if (isProcessing) return;
|
if (isProcessing) return;
|
||||||
if (attachmentComposer.pending) return;
|
if (attachmentComposer.pending) return;
|
||||||
const attachments = attachmentComposer.attachments;
|
const attachments = retryMessage?.attachments || attachmentComposer.attachments;
|
||||||
const message = msgInput.value.trim() || (attachments.length ? "Describe the attached images." : "");
|
const message = retryMessage?.content || msgInput.value.trim() || (attachments.length ? "Describe the attached images." : "");
|
||||||
if (!message) return;
|
if (!message) return;
|
||||||
|
if (retryMessage && !msgInput.value.trim()) {
|
||||||
|
msgInput.value = message;
|
||||||
|
resizeComposerInput();
|
||||||
|
}
|
||||||
const webSearch = webSearchToggle.enabled;
|
const webSearch = webSearchToggle.enabled;
|
||||||
const reasoning = reasoningToggle.enabled;
|
const reasoning = reasoningToggle.enabled;
|
||||||
if (!activeChatId) await createNewChat({preserveComposer: true});
|
if (!activeChatId) await createNewChat({preserveComposer: true});
|
||||||
@@ -1868,25 +1973,31 @@ async function sendMessage() {
|
|||||||
currentChat.title = autoTitle; updateWorkspaceCopy(); renderChatList(); await put(`/chats/${activeChatId}`, { title: autoTitle });
|
currentChat.title = autoTitle; updateWorkspaceCopy(); renderChatList(); await put(`/chats/${activeChatId}`, { title: autoTitle });
|
||||||
}
|
}
|
||||||
const userMessage = { role: "user", content: message, attachments };
|
const userMessage = { role: "user", content: message, attachments };
|
||||||
currentMessages.push(userMessage); addMessage(userMessage, true); updateChatActionState();
|
currentMessages.push(userMessage);
|
||||||
msgInput.value = "";
|
const userCard = addMessage(userMessage, true);
|
||||||
resizeComposerInput();
|
updateChatActionState();
|
||||||
setLoadingState(true, false); setNotice("Generating reply...");
|
setLoadingState(true, false); setNotice("Generating reply...");
|
||||||
const loadingMsg = addMessage({ role: "bot", content: "" }, false, true, { isNewest: true });
|
const loadingMsg = addMessage({ role: "bot", content: "" }, false, true, { isNewest: true });
|
||||||
const loadingBody = loadingMsg.querySelector(".msg-content");
|
const loadingBody = loadingMsg.querySelector(".msg-content");
|
||||||
const loadingShell = loadingMsg.querySelector(".loading-shell");
|
const loadingShell = loadingMsg.querySelector(".loading-shell");
|
||||||
const loadingLabel = loadingMsg.querySelector(".loading-copy strong");
|
const loadingLabel = loadingMsg.querySelector(".loading-copy strong");
|
||||||
loadingLabel.textContent = "Preparing response...";
|
loadingLabel.textContent = "Preparing response...";
|
||||||
|
const showSendFailure = (errorMessage) => {
|
||||||
|
if (!msgInput.value.trim()) {
|
||||||
|
msgInput.value = message;
|
||||||
|
resizeComposerInput();
|
||||||
|
}
|
||||||
|
userMessage.deliveryStatus = "failed";
|
||||||
|
userMessage.errorMessage = errorMessage;
|
||||||
|
userCard.replaceWith(addMessage(userMessage, true, false, { detached: true }));
|
||||||
|
setNotice(errorMessage, "error");
|
||||||
|
};
|
||||||
|
let completedReply = null;
|
||||||
try {
|
try {
|
||||||
let streamError = null;
|
let streamError = null;
|
||||||
await stream("/chat/stream", { message, attachments: attachments.map(item => item.id), model: modelSelect.value, chatId: activeChatId, webSearch, reasoning }, {
|
await stream("/chat/stream", { message, attachments: attachments.map(item => item.id), model: modelSelect.value, chatId: activeChatId, webSearch, reasoning }, {
|
||||||
onStatus: ({ status, sourceCount, searchState }) => {
|
onStatus: ({ status, sourceCount, searchState }) => {
|
||||||
if (status === "accepted") {
|
if (status === "accepted") return;
|
||||||
attachmentComposer.clear();
|
|
||||||
webSearchToggle.reset();
|
|
||||||
reasoningToggle.reset();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
loadingLabel.textContent = status === "analyzing" ? "Analyzing images..." : status === "searching" ? "Searching the web..."
|
loadingLabel.textContent = status === "analyzing" ? "Analyzing images..." : status === "searching" ? "Searching the web..."
|
||||||
: searchState === "no_relevant_results" ? "No sufficiently relevant results. Generating response..."
|
: searchState === "no_relevant_results" ? "No sufficiently relevant results. Generating response..."
|
||||||
: sourceCount === 0 ? "No web results found. Generating response..." : "Generating response...";
|
: sourceCount === 0 ? "No web results found. Generating response..." : "Generating response...";
|
||||||
@@ -1909,7 +2020,10 @@ async function sendMessage() {
|
|||||||
}
|
}
|
||||||
if (follow) scrollMessagesToBottom();
|
if (follow) scrollMessagesToBottom();
|
||||||
},
|
},
|
||||||
onDone: () => { webSearchToggle.reset(); reasoningToggle.reset(); finishReasoning(loadingMsg); },
|
onDone: ({ reply }) => {
|
||||||
|
completedReply = reply;
|
||||||
|
webSearchToggle.reset(); reasoningToggle.reset(); finishReasoning(loadingMsg);
|
||||||
|
},
|
||||||
onError: ({ error }) => {
|
onError: ({ error }) => {
|
||||||
streamError = error || "Failed to generate reply.";
|
streamError = error || "Failed to generate reply.";
|
||||||
}
|
}
|
||||||
@@ -1918,14 +2032,16 @@ async function sendMessage() {
|
|||||||
const previousScrollTop = messagesDiv.scrollTop;
|
const previousScrollTop = messagesDiv.scrollTop;
|
||||||
if (messagesDiv.contains(loadingMsg)) messagesDiv.removeChild(loadingMsg);
|
if (messagesDiv.contains(loadingMsg)) messagesDiv.removeChild(loadingMsg);
|
||||||
if (streamError) {
|
if (streamError) {
|
||||||
if (webSearch) {
|
showSendFailure(streamError);
|
||||||
msgInput.value = message;
|
|
||||||
resizeComposerInput();
|
|
||||||
}
|
|
||||||
addMessage(`[Error: ${streamError}]`);
|
|
||||||
setNotice(streamError, "error");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (msgInput.value.trim() === message) {
|
||||||
|
msgInput.value = "";
|
||||||
|
resizeComposerInput();
|
||||||
|
}
|
||||||
|
const composerAttachments = attachmentComposer.attachments;
|
||||||
|
if (composerAttachments.length === attachments.length
|
||||||
|
&& composerAttachments.every((item, index) => item.id === attachments[index].id)) attachmentComposer.clear();
|
||||||
await setActiveChat(activeChatId);
|
await setActiveChat(activeChatId);
|
||||||
await loadSummary();
|
await loadSummary();
|
||||||
markOnboardingCompleted();
|
markOnboardingCompleted();
|
||||||
@@ -1934,12 +2050,16 @@ async function sendMessage() {
|
|||||||
setNotice("Reply received.", "success");
|
setNotice("Reply received.", "success");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (messagesDiv.contains(loadingMsg)) messagesDiv.removeChild(loadingMsg);
|
if (messagesDiv.contains(loadingMsg)) messagesDiv.removeChild(loadingMsg);
|
||||||
if (webSearch) {
|
|
||||||
msgInput.value = message;
|
|
||||||
resizeComposerInput();
|
|
||||||
}
|
|
||||||
const errorMessage = error?.message || "Network error while sending message.";
|
const errorMessage = error?.message || "Network error while sending message.";
|
||||||
addMessage(`[Error: ${errorMessage}]`); setNotice(errorMessage, "error");
|
if (completedReply !== null) {
|
||||||
|
if (userCard.isConnected) {
|
||||||
|
currentMessages.push({ role: "bot", content: completedReply });
|
||||||
|
addMessage(completedReply);
|
||||||
|
}
|
||||||
|
setNotice(`Reply sent, but the conversation could not be refreshed: ${errorMessage}`, "error");
|
||||||
|
} else {
|
||||||
|
showSendFailure(errorMessage);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingState(false); msgInput.focus(); updateChatActionState();
|
setLoadingState(false); msgInput.focus(); updateChatActionState();
|
||||||
}
|
}
|
||||||
@@ -1996,7 +2116,7 @@ function bindChatDrawerActions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sendBtn.addEventListener("click", () => { void (isProcessing ? stopGeneration() : sendMessage()); });
|
sendBtn.addEventListener("click", () => { void (isProcessing ? stopGeneration() : sendMessage()); });
|
||||||
msgInput.addEventListener("keydown", (event) => { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); void sendMessage(); } });
|
msgInput.addEventListener("keydown", (event) => { if (event.key === "Enter" && !event.shiftKey && !event.isComposing) { event.preventDefault(); void sendMessage(); } });
|
||||||
msgInput.addEventListener("input", function () {
|
msgInput.addEventListener("input", function () {
|
||||||
resizeComposerInput();
|
resizeComposerInput();
|
||||||
updateSendState();
|
updateSendState();
|
||||||
@@ -2019,7 +2139,7 @@ if (importWorkspaceBtn) importWorkspaceBtn.addEventListener("click", () => {
|
|||||||
if (importWorkspaceFileInput) importWorkspaceFileInput.addEventListener("change", () => {
|
if (importWorkspaceFileInput) importWorkspaceFileInput.addEventListener("change", () => {
|
||||||
void importWorkspaceFromFile(importWorkspaceFileInput.files?.[0]);
|
void importWorkspaceFromFile(importWorkspaceFileInput.files?.[0]);
|
||||||
});
|
});
|
||||||
chatSearchInput.addEventListener("input", () => { void loadChatSessions(); });
|
chatSearchInput.addEventListener("input", scheduleChatSearch);
|
||||||
modelSelect.addEventListener("change", () => {
|
modelSelect.addEventListener("change", () => {
|
||||||
renderModelSection();
|
renderModelSection();
|
||||||
void saveSelectedModel();
|
void saveSelectedModel();
|
||||||
@@ -2072,9 +2192,11 @@ popupInput.addEventListener("keydown", (event) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
bindChatDrawerActions();
|
bindChatDrawerActions();
|
||||||
|
if (chatSidebar) chatSidebar.inert = isMobileLayout();
|
||||||
if (chatSidebar) {
|
if (chatSidebar) {
|
||||||
chatSidebar.addEventListener("click", (event) => event.stopPropagation());
|
chatSidebar.addEventListener("click", (event) => event.stopPropagation());
|
||||||
}
|
}
|
||||||
|
document.getElementById("chatSidebarBackdrop")?.addEventListener("click", closeMobileSidebar);
|
||||||
if (chatSidebarToggle) {
|
if (chatSidebarToggle) {
|
||||||
chatSidebarToggle.addEventListener("click", (event) => {
|
chatSidebarToggle.addEventListener("click", (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -2105,14 +2227,33 @@ if (onboardingModal) {
|
|||||||
if (event.target === onboardingModal) markOnboardingCompleted();
|
if (event.target === onboardingModal) markOnboardingCompleted();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
document.addEventListener("click", () => {
|
document.addEventListener("click", (event) => {
|
||||||
|
if (!event.target.closest(".msg-action-menu")) {
|
||||||
|
document.querySelectorAll(".msg-action-menu[open]").forEach((menu) => { menu.open = false; });
|
||||||
|
}
|
||||||
closeMobileSidebar();
|
closeMobileSidebar();
|
||||||
});
|
});
|
||||||
window.addEventListener("resize", () => {
|
window.addEventListener("resize", () => {
|
||||||
if (!isMobileLayout()) closeMobileSidebar();
|
if (!isMobileLayout()) closeMobileSidebar();
|
||||||
|
if (chatSidebar) chatSidebar.inert = isMobileLayout() && !document.body.classList.contains("sidebar-open");
|
||||||
});
|
});
|
||||||
document.addEventListener("keydown", (event) => {
|
document.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Tab" && isMobileLayout() && document.body.classList.contains("sidebar-open")) {
|
||||||
|
const focusables = [...chatSidebar.querySelectorAll('button:not([disabled]), input:not([disabled]), [tabindex="0"]')]
|
||||||
|
.filter((element) => !element.hidden && !element.closest("[hidden]"));
|
||||||
|
const first = focusables[0], last = focusables.at(-1);
|
||||||
|
if (event.shiftKey && (document.activeElement === first || !chatSidebar.contains(document.activeElement))) {
|
||||||
|
event.preventDefault(); last?.focus();
|
||||||
|
} else if (!event.shiftKey && (document.activeElement === last || !chatSidebar.contains(document.activeElement))) {
|
||||||
|
event.preventDefault(); first?.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
|
const openMenu = document.querySelector(".msg-action-menu[open]");
|
||||||
|
if (openMenu) {
|
||||||
|
openMenu.open = false;
|
||||||
|
openMenu.querySelector("summary")?.focus();
|
||||||
|
}
|
||||||
closeMobileSidebar();
|
closeMobileSidebar();
|
||||||
closeRoleplayStarter();
|
closeRoleplayStarter();
|
||||||
closePersonaForm();
|
closePersonaForm();
|
||||||
|
|||||||
Reference in New Issue
Block a user