make huge overhauls to backend

This commit is contained in:
2026-07-02 12:58:14 +02:00
parent 66f2407473
commit 65e4162651
18 changed files with 1159 additions and 14 deletions
+102
View File
@@ -26,6 +26,9 @@ import {
backupWorkspaceBtn,
clearUserPersonaBtn,
exportChatBtn,
importWorkspaceBtn,
manageChatMemoryBtn,
importWorkspaceFileInput,
moveChatFolderBtn,
loginForm,
loginPasswordInput,
@@ -891,6 +894,7 @@ function updateChatActionState() {
if (hasChat) pinChatBtn.textContent = activeChat.is_pinned ? "Unpin" : "Pin";
}
if (moveChatFolderBtn) moveChatFolderBtn.disabled = !hasChat;
if (manageChatMemoryBtn) manageChatMemoryBtn.disabled = !hasChat;
if (archiveChatBtn) {
archiveChatBtn.disabled = !hasChat;
if (hasChat) archiveChatBtn.textContent = activeChat.archived_at ? "Unarchive" : "Archive";
@@ -898,6 +902,7 @@ function updateChatActionState() {
clearChatBtn.disabled = !hasChat;
exportChatBtn.disabled = !hasChat || currentMessages.length === 0;
if (backupWorkspaceBtn) backupWorkspaceBtn.disabled = !currentUsername;
if (importWorkspaceBtn) importWorkspaceBtn.disabled = !currentUsername;
}
function renderPersonaSection() {
@@ -1497,6 +1502,92 @@ async function backupWorkspace() {
setNotice("Workspace backup downloaded.", "success");
}
function formatImportPreview(preview, warnings = []) {
const counts = [
`${preview.personas || 0} persona${preview.personas === 1 ? "" : "s"}`,
`${preview.chats || 0} chat${preview.chats === 1 ? "" : "s"}`,
`${preview.messages || 0} message${preview.messages === 1 ? "" : "s"}`,
`${preview.templates || 0} template${preview.templates === 1 ? "" : "s"}`
].join(", ");
const warningText = warnings.length ? ` Warnings: ${warnings.slice(0, 4).join("; ")}${warnings.length > 4 ? "; ..." : ""}` : "";
return `Import preview: ${counts}.${warningText}`;
}
async function importWorkspaceFromFile(file) {
if (!file) return;
setChatLoading(true, "Reading workspace backup...");
let workspace;
try {
workspace = JSON.parse(await file.text());
} catch {
setChatLoading(false);
return setNotice("Import failed: choose a valid JSON workspace backup.", "error");
}
const preview = await post("/imports/workspace/preview", {workspace});
if (preview.error || preview.ok === false) {
setChatLoading(false);
const detail = (preview.errors || []).slice(0, 4).join("; ");
return setNotice(detail || preview.error || "Workspace import validation failed.", "error");
}
setChatLoading(false);
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...");
const imported = await post("/imports/workspace", {
workspace,
allowDuplicates: Boolean(preview.preview?.duplicates?.length)
});
if (imported.error) {
setChatLoading(false);
const detail = (imported.errors || imported.warnings || []).slice(0, 4).join("; ");
return setNotice(detail || imported.error, "error");
}
await Promise.all([loadPersonas(), loadChatSessions()]);
setChatLoading(false);
setNotice(`Workspace imported: ${imported.imported.personas} personas, ${imported.imported.chats} chats, ${imported.imported.templates} templates.`, "success");
}
function formatMemoryList(memories) {
if (!memories.length) return "No memory facts saved for this chat yet.";
return memories.map((memory, index) => `${index + 1}. ${memory.fact}`).join("\n");
}
async function manageChatMemory() {
if (!activeChatId) return;
const loaded = await get(`/chats/${activeChatId}/memories`);
if (loaded.error) return setNotice(loaded.error, "error");
const memories = loaded.memories || [];
const action = await promptPopup({
eyebrow: "Memory",
title: "Memory facts",
description: `${formatMemoryList(memories)}\n\nType a new fact to add it, or type delete 1 to remove a fact.`,
label: "Fact or command",
placeholder: "The user prefers terse answers.",
confirmLabel: "Save"
});
const value = String(action || "").trim();
if (!value) return;
const deleteMatch = value.match(/^delete\s+(\d+)$/i);
if (deleteMatch) {
const target = memories[Number(deleteMatch[1]) - 1];
if (!target) return setNotice("Memory number not found.", "error");
const removed = await del(`/chats/${activeChatId}/memories/${target.id}`);
if (removed.error) return setNotice(removed.error, "error");
return setNotice("Memory fact deleted.", "success");
}
const saved = await post(`/chats/${activeChatId}/memories`, {fact: value});
if (saved.error) return setNotice(saved.error, "error");
return setNotice("Memory fact saved.", "success");
}
async function deleteChat(id) {
setChatLoading(true, "Deleting chat...");
const chat = getChatById(id);
@@ -1652,6 +1743,9 @@ const chatActions = {
if (folderName === null) return;
await updateChatOrganization(chat.id, {folderName: String(folderName || "").trim() || null});
},
memory: async () => {
await manageChatMemory();
},
pin: async () => {
const chat = getChatById(activeChatId);
if (chat) await updateChatOrganization(chat.id, {isPinned: !chat.is_pinned});
@@ -1700,6 +1794,14 @@ if (backupWorkspaceBtn) backupWorkspaceBtn.addEventListener("click", () => {
closeChatDrawer();
void backupWorkspace();
});
if (importWorkspaceBtn) importWorkspaceBtn.addEventListener("click", () => {
closeChatDrawer();
importWorkspaceFileInput.value = "";
importWorkspaceFileInput.click();
});
if (importWorkspaceFileInput) importWorkspaceFileInput.addEventListener("change", () => {
void importWorkspaceFromFile(importWorkspaceFileInput.files?.[0]);
});
chatSearchInput.addEventListener("input", () => { void loadChatSessions(); });
modelSelect.addEventListener("change", () => {
renderModelSection();