make some overall QoL improvements
This commit is contained in:
+56
-35
@@ -7,6 +7,7 @@ import { appendSources, createWebSearchToggle } from "./webSearch.js";
|
||||
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 { openModal, closeModal, topModal } from "./modalFocus.js";
|
||||
import { onboardingPrompts, requestedChatId } from "./constants.js?v=20260909-account-controls";
|
||||
import { getModelProfile } from "./modelProfiles.js?v=20260909-account-controls";
|
||||
import { applyTheme as applySharedTheme, applyThemeMode as applySharedThemeMode, readStoredAppearance } from "./themeController.js?v=20260909-account-controls";
|
||||
@@ -243,7 +244,7 @@ function markOnboardingCompleted() {
|
||||
|
||||
function closeOnboarding() {
|
||||
if (!onboardingModal) return;
|
||||
onboardingModal.classList.add("hidden");
|
||||
closeModal(onboardingModal);
|
||||
}
|
||||
|
||||
function renderOnboardingStep() {
|
||||
@@ -262,6 +263,7 @@ function renderOnboardingStep() {
|
||||
button.addEventListener("click", () => {
|
||||
onboardingIntent = key;
|
||||
renderOnboardingStep();
|
||||
onboardingChoices.querySelector(".active")?.focus();
|
||||
});
|
||||
onboardingChoices.appendChild(button);
|
||||
});
|
||||
@@ -299,7 +301,7 @@ function maybeShowOnboarding() {
|
||||
onboardingStep = 1;
|
||||
onboardingIntent = "ask";
|
||||
renderOnboardingStep();
|
||||
onboardingModal.classList.remove("hidden");
|
||||
openModal(onboardingModal, onboardingChoices.querySelector("button"));
|
||||
}
|
||||
function setMessageContent(el, content) {
|
||||
const markdownParser = window.marked;
|
||||
@@ -311,11 +313,6 @@ function setMessageContent(el, content) {
|
||||
}
|
||||
}
|
||||
function getChatById(id) { return chatSessions.find((chat) => chat.id === id); }
|
||||
function summarizeText(value, limit = 96) {
|
||||
const normalized = String(value || "").replace(/\s+/g, " ").trim();
|
||||
if (!normalized) return "";
|
||||
return normalized.length > limit ? `${normalized.slice(0, limit - 3).trimEnd()}...` : normalized;
|
||||
}
|
||||
function getPersonaName(id, items) {
|
||||
return items.find((persona) => persona.id === id)?.name || "";
|
||||
}
|
||||
@@ -434,10 +431,6 @@ function updateJumpToLatest() {
|
||||
messagesDiv.addEventListener("scroll", updateJumpToLatest, {passive: true});
|
||||
jumpToLatest.addEventListener("click", scrollMessagesToBottom);
|
||||
|
||||
function getNewestMessage() {
|
||||
return currentMessages.length ? currentMessages[currentMessages.length - 1] : null;
|
||||
}
|
||||
|
||||
function getMessageIndexById(messageId) {
|
||||
return currentMessages.findIndex((message) => message.id === messageId);
|
||||
}
|
||||
@@ -505,15 +498,6 @@ function syncRetryStateFromMessage(message) {
|
||||
return state;
|
||||
}
|
||||
|
||||
function ensureRetryState(message) {
|
||||
if (!message?.id) return null;
|
||||
let state = messageRetryState.get(message.id);
|
||||
if (!state) {
|
||||
state = syncRetryStateFromMessage(message);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
async function openCreatedChat(chat) {
|
||||
if (!chat?.id) throw new Error("The server did not return the newly created chat");
|
||||
if (chatSearchInput) {
|
||||
@@ -587,7 +571,7 @@ async function editChatMessage(messageId, fallbackIndex = -1) {
|
||||
async function deleteChatMessage(messageId, fallbackIndex = -1) {
|
||||
const target = await resolveMessageTarget(messageId, fallbackIndex);
|
||||
if (!target) return setNotice("Message is not ready yet. Try again.", "error");
|
||||
const { index, message } = target;
|
||||
const { message } = target;
|
||||
const confirmed = await confirmPopup({
|
||||
eyebrow: message.role === "user" ? "Your message" : "Assistant message",
|
||||
title: "Delete message",
|
||||
@@ -1246,7 +1230,7 @@ function updateActivePersonaStatus(items, activeId, statusEl, emptyText, prefix)
|
||||
function openPersonaForm(persona = null, personaType = "assistant") {
|
||||
closeChatDrawer();
|
||||
closePopup(false);
|
||||
personaModal.classList.remove("hidden");
|
||||
openModal(personaModal, personaNameInput);
|
||||
if (persona) {
|
||||
editingPersonaId = persona.id; editingPersonaType = persona.persona_type || "assistant";
|
||||
personaFormTitle.textContent = `Edit ${editingPersonaType === "user" ? "your persona" : "AI Character"}`;
|
||||
@@ -1266,7 +1250,7 @@ function openPersonaForm(persona = null, personaType = "assistant") {
|
||||
personaNameInput.focus();
|
||||
}
|
||||
|
||||
function closePersonaForm() { personaModal.classList.add("hidden"); editingPersonaId = null; editingPersonaType = "assistant"; personaTypeSelect.disabled = false; setPersonaFormNotice(""); }
|
||||
function closePersonaForm() { closeModal(personaModal); editingPersonaId = null; editingPersonaType = "assistant"; personaTypeSelect.disabled = false; setPersonaFormNotice(""); }
|
||||
function populateRoleplayStarter() {
|
||||
if (!roleplayCharacterSelect || !roleplayUserPersonaSelect) return;
|
||||
roleplayCharacterSelect.innerHTML = "";
|
||||
@@ -1315,12 +1299,11 @@ function openRoleplayStarter(characterId = null) {
|
||||
populateRoleplayStarter();
|
||||
if (roleplayScenarioInput) roleplayScenarioInput.value = "";
|
||||
setRoleplayStarterNotice("");
|
||||
roleplayStarterModal.classList.remove("hidden");
|
||||
if (roleplayCharacterSelect.options.length) roleplayCharacterSelect.focus();
|
||||
openModal(roleplayStarterModal, roleplayCharacterSelect);
|
||||
}
|
||||
function closeRoleplayStarter() {
|
||||
if (!roleplayStarterModal) return;
|
||||
roleplayStarterModal.classList.add("hidden");
|
||||
closeModal(roleplayStarterModal);
|
||||
roleplayPresetCharacterId = null;
|
||||
roleplayStarterStep = 1;
|
||||
roleplayStarterConfirm.disabled = false;
|
||||
@@ -1349,7 +1332,7 @@ function finishPopup(result) {
|
||||
}
|
||||
function closePopup(resolveValue = null) {
|
||||
if (!popupModal || popupModal.classList.contains("hidden")) return;
|
||||
popupModal.classList.add("hidden");
|
||||
closeModal(popupModal);
|
||||
popupField.classList.add("hidden");
|
||||
popupInput.value = "";
|
||||
popupConfirmBtn.disabled = false;
|
||||
@@ -1377,7 +1360,7 @@ function openPopup(options) {
|
||||
} else {
|
||||
popupInput.value = "";
|
||||
}
|
||||
popupModal.classList.remove("hidden");
|
||||
openModal(popupModal, options.mode === "prompt" ? popupInput : popupConfirmBtn);
|
||||
return new Promise((resolve) => {
|
||||
popupResolver = resolve;
|
||||
requestAnimationFrame(() => {
|
||||
@@ -1801,18 +1784,18 @@ function exportCurrentChat() {
|
||||
}
|
||||
|
||||
async function backupWorkspace() {
|
||||
const response = await fetch("/exports/workspace");
|
||||
const data = await response.json();
|
||||
const blob = new Blob([JSON.stringify(data.workspace, null, 2)], { type: "application/json;charset=utf-8" });
|
||||
const response = await fetch("/exports/workspace/archive");
|
||||
if (!response.ok) return setNotice("Workspace archive could not be created.", "error");
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = `workspace-backup-${new Date().toISOString().slice(0, 10)}.json`;
|
||||
link.download = `workspace-backup-${new Date().toISOString().slice(0, 10)}.tar.gz`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
setNotice("Workspace backup downloaded.", "success");
|
||||
setNotice("Workspace archive downloaded with images.", "success");
|
||||
}
|
||||
|
||||
function formatImportPreview(preview, warnings = []) {
|
||||
@@ -1829,6 +1812,31 @@ function formatImportPreview(preview, warnings = []) {
|
||||
async function importWorkspaceFromFile(file) {
|
||||
if (!file) return;
|
||||
setChatLoading(true, "Reading workspace backup...");
|
||||
const isArchive = file.name.toLowerCase().endsWith(".tar.gz");
|
||||
if (isArchive) {
|
||||
const submit = async (path, allowDuplicates = false) => {
|
||||
const body = new FormData();
|
||||
body.append("archive", file);
|
||||
if (allowDuplicates) body.append("allowDuplicates", "true");
|
||||
const response = await fetch(path, {method: "POST", body});
|
||||
return response.json();
|
||||
};
|
||||
try {
|
||||
const preview = await submit("/imports/workspace/archive/preview");
|
||||
setChatLoading(false);
|
||||
if (preview.error || preview.ok === false) return setNotice((preview.errors || []).slice(0, 4).join("; ") || preview.error || "Invalid archive.", "error");
|
||||
const confirmed = await confirmPopup({eyebrow: "Workspace import", title: "Review import",
|
||||
description: formatImportPreview(preview.preview || {}, preview.warnings || []), confirmLabel: "Import"});
|
||||
if (!confirmed) return;
|
||||
setChatLoading(true, "Importing workspace and images...");
|
||||
const result = await submit("/imports/workspace/archive", Boolean(preview.preview?.duplicates?.length));
|
||||
if (result.error) return setNotice(result.error, "error");
|
||||
await Promise.all([loadPersonas(), loadChatSessions()]);
|
||||
setNotice(`Workspace imported: ${result.imported.chats} chats with their images.`, "success");
|
||||
} catch { setNotice("Workspace archive could not be imported.", "error"); }
|
||||
finally { setChatLoading(false); }
|
||||
return;
|
||||
}
|
||||
let workspace;
|
||||
try {
|
||||
workspace = JSON.parse(await file.text());
|
||||
@@ -2020,7 +2028,9 @@ async function checkSession() {
|
||||
// Starters are offered on workspace home.
|
||||
const starter = new URLSearchParams(location.search).get("prompt");
|
||||
if (starter) { msgInput.value = starter; resizeComposerInput(); updateSendState(); }
|
||||
msgInput.focus(); return true;
|
||||
msgInput.focus();
|
||||
if (!starter && !activeChatId) maybeShowOnboarding();
|
||||
return true;
|
||||
}
|
||||
|
||||
async function sendMessage({ retryMessage = null } = {}) {
|
||||
@@ -2219,7 +2229,7 @@ if (headerModelButton) headerModelButton.addEventListener("click", (event) => {
|
||||
openModelSettings();
|
||||
});
|
||||
saveGenerationSettingsBtn.addEventListener("click", () => { void saveGenerationSettings(); });
|
||||
roleplayNewPersonaBtn.addEventListener("click", () => openPersonaForm(null, "assistant"));
|
||||
roleplayNewPersonaBtn.addEventListener("click", () => { closeRoleplayStarter(); openPersonaForm(null, "assistant"); });
|
||||
clearUserPersonaBtn.addEventListener("click", () => {
|
||||
closeChatDrawer();
|
||||
void clearPersona("user");
|
||||
@@ -2281,12 +2291,14 @@ if (onboardingContinue) {
|
||||
onboardingContinue.addEventListener("click", () => {
|
||||
onboardingStep = 2;
|
||||
renderOnboardingStep();
|
||||
onboardingChoices.querySelector("button")?.focus();
|
||||
});
|
||||
}
|
||||
if (onboardingBack) {
|
||||
onboardingBack.addEventListener("click", () => {
|
||||
onboardingStep = 1;
|
||||
renderOnboardingStep();
|
||||
(onboardingChoices.querySelector(".active") || onboardingChoices.querySelector("button"))?.focus();
|
||||
});
|
||||
}
|
||||
if (onboardingSkip) {
|
||||
@@ -2319,6 +2331,15 @@ document.addEventListener("keydown", (event) => {
|
||||
}
|
||||
}
|
||||
if (event.key === "Escape") {
|
||||
const activeModal = topModal();
|
||||
if (activeModal) {
|
||||
event.preventDefault();
|
||||
if (activeModal === popupModal) closePopup(false);
|
||||
else if (activeModal === personaModal) closePersonaForm();
|
||||
else if (activeModal === roleplayStarterModal) closeRoleplayStarter();
|
||||
else if (activeModal === onboardingModal) closeOnboarding();
|
||||
return;
|
||||
}
|
||||
const openMenu = document.querySelector(".msg-action-menu[open]");
|
||||
if (openMenu) {
|
||||
openMenu.open = false;
|
||||
|
||||
Reference in New Issue
Block a user