remake menu section and fix styling issues

This commit is contained in:
2026-04-15 21:40:24 +02:00
parent 481e0f10e3
commit 7cff596749
5 changed files with 386 additions and 274 deletions
+58
View File
@@ -0,0 +1,58 @@
import {chatDrawer, chatDrawerButton, chatDrawerCloseBtn} from "./dom.js";
function syncChatDrawerState(isOpen) {
if (!chatDrawer || !chatDrawerButton) return;
if (isOpen) document.body.classList.remove("sidebar-open");
chatDrawer.classList.toggle("hidden", !isOpen);
chatDrawerButton.setAttribute("aria-expanded", String(isOpen));
document.body.classList.toggle("chat-drawer-open", isOpen);
}
function isChatDrawerOpen() {
return Boolean(chatDrawer && !chatDrawer.classList.contains("hidden"));
}
function openChatDrawer() {
syncChatDrawerState(true);
}
function closeChatDrawer() {
syncChatDrawerState(false);
}
function toggleChatDrawer() {
syncChatDrawerState(!isChatDrawerOpen());
}
function handleDocumentClick(event) {
if (!isChatDrawerOpen()) return;
const target = event.target;
if (!(target instanceof Node)) return;
if (chatDrawerButton?.contains(target)) return;
if (chatDrawer?.querySelector(".chat-drawer-panel")?.contains(target)) return;
closeChatDrawer();
}
function handleDocumentKeydown(event) {
if (event.key === "Escape") closeChatDrawer();
}
if (chatDrawerButton && chatDrawer) {
chatDrawerButton.addEventListener("click", () => {
toggleChatDrawer();
});
}
if (chatDrawerCloseBtn) {
chatDrawerCloseBtn.addEventListener("click", closeChatDrawer);
}
document.addEventListener("click", handleDocumentClick);
document.addEventListener("keydown", handleDocumentKeydown);
export {
closeChatDrawer,
isChatDrawerOpen,
openChatDrawer,
toggleChatDrawer
};
+14 -26
View File
@@ -6,13 +6,13 @@ const authMsg = $("authMsg");
const logoutButton = $("logout");
const messagesDiv = $("messages");
const modelSelect = $("model");
const modelMenuButton = $("modelMenuButton");
const modelPopover = $("modelPopover");
const modelPanel = $("modelPanel");
const modelBadgeName = $("modelBadgeName");
const modelCount = $("modelCount");
const modelHelpTitle = $("modelHelpTitle");
const modelHelpBadge = $("modelHelpBadge");
const modelHelpSummary = $("modelHelpSummary");
const chatDrawerPersonaValue = $("chatDrawerPersonaValue");
const loginForm = $("loginForm");
const registerForm = $("registerForm");
const msgInput = $("msgInput");
@@ -27,6 +27,8 @@ const chatList = $("chatList");
const chatSearchInput = $("chatSearch");
const chatSidebar = $("chatSidebar");
const chatDrawerCloseBtn = $("chatDrawerClose");
const chatDrawerButton = $("chatDrawerButton");
const chatDrawer = $("chatDrawer");
const newChatBtn = $("newChat");
const chatListLoading = $("chatListLoading");
const chatListLoadingText = $("chatListLoadingText");
@@ -43,14 +45,8 @@ const chatActivityOverlay = $("chatActivityOverlay");
const chatActivityEyebrow = $("chatActivityEyebrow");
const chatActivityTitle = $("chatActivityTitle");
const chatActivityDetail = $("chatActivityDetail");
const contextChipButton = $("contextChipButton");
const contextPopover = $("contextPopover");
const contextSummaryLabel = $("contextSummaryLabel");
const chatActionsMenuButton = $("chatActionsMenuButton");
const chatActionsPopover = $("chatActionsPopover");
const chatCharacterPill = $("chatCharacterPill");
const chatUserPersonaPill = $("chatUserPersonaPill");
const personaList = $("personaList");
const userPersonaList = $("userPersonaList");
const personaForm = $("personaForm");
const personaFormTitle = $("personaFormTitle");
@@ -65,12 +61,10 @@ const personaExampleDialoguesInput = $("personaExampleDialogues");
const personaFormNotice = $("personaFormNotice");
const roleplayNewPersonaBtn = $("roleplayNewPersona");
const clearUserPersonaBtn = $("clearUserPersona");
const activePersonaStatus = $("activePersonaStatus");
const activeUserPersonaStatus = $("activeUserPersonaStatus");
const personaModal = $("personaModal");
const personaCloseBtn = $("personaClose");
const personaMenuButton = $("personaMenuButton");
const personaPopover = $("personaPopover");
const personaPanel = $("personaPanel");
const roleplayStarterModal = $("roleplayStarterModal");
const roleplayStarterClose = $("roleplayStarterClose");
const roleplayStarterCancel = $("roleplayStarterCancel");
@@ -106,24 +100,24 @@ const authScreens = document.querySelectorAll(".auth-screen");
const toggleButtons = document.querySelectorAll(".auth-toggle .toggle");
const themeNameTargets = document.querySelectorAll("[data-theme-name]");
const themeLogoTargets = document.querySelectorAll("[data-theme-logo]");
const modelMenu = document.querySelector(".model-menu");
const personaMenu = document.querySelector(".persona-menu");
const modelSection = document.querySelector(".chat-drawer-section--model");
const personaSection = document.querySelector(".chat-drawer-section--persona");
export {
$,
activeChatTitle,
activePersonaStatus,
activeUserPersonaStatus,
authDiv,
authMsg,
authScreens,
chatActionsMenuButton,
chatActionsPopover,
chatActivityDetail,
chatActivityEyebrow,
chatActivityOverlay,
chatActivityTitle,
chatCharacterPill,
chatDrawerPersonaValue,
chatDrawer,
chatDrawerButton,
chatDiv,
chatList,
chatListLoading,
@@ -134,9 +128,6 @@ export {
chatUserPersonaPill,
clearChatBtn,
clearUserPersonaBtn,
contextChipButton,
contextPopover,
contextSummaryLabel,
exportChatBtn,
pinChatBtn,
moveChatFolderBtn,
@@ -153,9 +144,8 @@ export {
modelHelpBadge,
modelHelpSummary,
modelHelpTitle,
modelMenu,
modelMenuButton,
modelPopover,
modelPanel,
modelSection,
modelSelect,
msgInput,
newChatBtn,
@@ -173,12 +163,10 @@ export {
personaForm,
personaFormNotice,
personaFormTitle,
personaList,
personaMenu,
personaMenuButton,
personaModal,
personaNameInput,
personaPopover,
personaPanel,
personaSection,
personaPronounsInput,
personaTypeSelect,
personaAppearanceInput,
+60 -39
View File
@@ -109,32 +109,38 @@
</div>
<div class="chat-header-actions">
<div class="chat-actions-menu">
<button id="chatActionsMenuButton" class="secondary-action overflow-trigger" type="button" aria-expanded="false" aria-controls="chatActionsPopover">
Menu
<button id="chatDrawerButton" class="secondary-action overflow-trigger" type="button" aria-expanded="false" aria-controls="chatDrawer">
<span class="overflow-trigger-label">Controls</span>
</button>
<div id="chatActionsPopover" class="chat-drawer hidden" role="dialog" aria-modal="false" aria-labelledby="chatDrawerTitle">
<div id="chatDrawer" class="chat-drawer hidden" role="dialog" aria-modal="false" aria-labelledby="chatDrawerTitle">
<div class="chat-drawer-panel">
<div class="chat-drawer-header">
<div>
<p class="eyebrow">Conversation</p>
<h2 id="chatDrawerTitle">Controls</h2>
<h2 id="chatDrawerTitle">Chat controls</h2>
</div>
<button id="chatDrawerClose" class="icon-button" type="button" aria-label="Close panel">Close</button>
</div>
<div class="chat-drawer-body">
<div class="chat-drawer-section chat-drawer-summary">
<div class="chat-drawer-summary">
<p class="dropdown-section-label">Current mode</p>
<div class="chat-drawer-pills">
<span id="chatCharacterPill" class="context-pill hidden">Character: none</span>
<span id="chatUserPersonaPill" class="context-pill hidden">User persona: none</span>
</div>
</div>
<div class="chat-drawer-section model-menu">
<div class="dropdown-header">
<p class="eyebrow">Model</p>
<p class="dropdown-subtitle">Choose the engine for this chat.</p>
</div>
<div id="modelPopover" class="model-popover" data-static-panel="true">
<details class="chat-drawer-section chat-drawer-disclosure chat-drawer-section--model">
<summary class="chat-drawer-row">
<span class="chat-drawer-row-label">
<span class="eyebrow">Model</span>
<strong>Engine</strong>
</span>
<span class="chat-drawer-row-meta">
<strong id="modelBadgeName">Loading...</strong>
<span class="chat-drawer-row-hint">Change</span>
</span>
</summary>
<div id="modelPanel" class="model-panel chat-drawer-disclosure-panel" data-static-panel="true">
<div class="model-select">
<div class="model-select-header">
<label for="model">Model</label>
@@ -154,18 +160,20 @@
</div>
<p id="modelHelpSummary" class="model-help-summary">Pick a model to see what it is best suited for.</p>
</div>
<div class="model-inline-badge">
<span class="model-badge-label">Current</span>
<strong id="modelBadgeName">Loading...</strong>
</div>
</div>
</div>
<div class="chat-drawer-section persona-menu">
<div class="dropdown-header">
<p class="eyebrow">Persona</p>
<p class="dropdown-subtitle">Set who you are in roleplay chats.</p>
</div>
<div id="personaPopover" class="persona-popover" data-static-panel="true">
</details>
<details class="chat-drawer-section chat-drawer-disclosure chat-drawer-section--persona">
<summary class="chat-drawer-row">
<span class="chat-drawer-row-label">
<span class="eyebrow">Persona</span>
<strong>Identity</strong>
</span>
<span class="chat-drawer-row-meta">
<strong id="chatDrawerPersonaValue">None</strong>
<span class="chat-drawer-row-hint">Change</span>
</span>
</summary>
<div id="personaPanel" class="persona-panel chat-drawer-disclosure-panel" data-static-panel="true">
<div class="persona-section">
<div class="persona-section-header">
<div>
@@ -180,27 +188,40 @@
<div id="userPersonaList" class="persona-list"></div>
</div>
</div>
</div>
<div class="chat-drawer-section">
<p class="dropdown-section-label">Workspace</p>
<div class="dropdown-action-grid">
<a href="/market" class="secondary-action">Market</a>
<a href="/settings" class="secondary-action">Settings</a>
<button id="logout" type="button" class="secondary-action">Logout</button>
<button id="backupWorkspace" class="secondary-action" type="button">Backup</button>
</div>
</div>
</details>
<div class="chat-drawer-section">
<p class="dropdown-section-label">Current chat</p>
<div class="dropdown-action-grid">
<button id="renameChat" class="secondary-action" type="button">Rename</button>
<button id="pinChat" class="secondary-action" type="button">Pin</button>
<button id="moveChatFolder" class="secondary-action" type="button">Folder</button>
<button id="archiveChat" class="secondary-action" type="button">Archive</button>
<button id="clearChat" class="secondary-action" type="button">Clear</button>
<button id="exportChat" class="secondary-action" type="button">Export</button>
<div class="chat-drawer-action-list">
<button id="renameChat" class="secondary-action chat-drawer-action" type="button" data-chat-action="rename">Rename chat</button>
<button id="pinChat" class="secondary-action chat-drawer-action" type="button" data-chat-action="pin">Pin</button>
<button id="archiveChat" class="secondary-action chat-drawer-action" type="button" data-chat-action="archive">Archive</button>
<button id="exportChat" class="secondary-action chat-drawer-action" type="button" data-chat-action="export">Export</button>
</div>
</div>
<details class="chat-drawer-section chat-drawer-more">
<summary class="chat-drawer-row chat-drawer-row--compact">
<span class="chat-drawer-row-label">
<strong>More actions</strong>
</span>
<span class="chat-drawer-row-hint">Expand</span>
</summary>
<div class="chat-drawer-action-list chat-drawer-disclosure-panel">
<button id="moveChatFolder" class="secondary-action chat-drawer-action" type="button" data-chat-action="folder">Move to folder</button>
<button id="backupWorkspace" class="secondary-action chat-drawer-action" type="button">Backup workspace</button>
<button id="logout" type="button" class="secondary-action chat-drawer-action">Logout</button>
</div>
</details>
<details class="chat-drawer-section chat-drawer-danger">
<summary class="chat-drawer-row chat-drawer-row--compact">
<span class="chat-drawer-row-label">
<strong>Danger zone</strong>
</span>
<span class="chat-drawer-row-hint">Expand</span>
</summary>
<div class="chat-drawer-action-list chat-drawer-disclosure-panel">
<button id="clearChat" class="secondary-action chat-drawer-action chat-drawer-action-danger" type="button" data-chat-action="clear">Clear chat</button>
</div>
</details>
</div>
</div>
</div>
+77 -180
View File
@@ -1,34 +1,29 @@
import {del, get, post, put, stream} from "./app/api.js";
import {closeChatDrawer} from "./app/chatDrawer.js";
import {defaultModelProfile, onboardingPrompts, requestedChatId, themes} from "./app/constants.js";
import {
activeChatTitle,
activePersonaStatus,
activeUserPersonaStatus,
authDiv,
authMsg,
authScreens,
chatActionsMenuButton,
chatActionsPopover,
chatActivityDetail,
chatActivityEyebrow,
chatActivityOverlay,
chatActivityTitle,
chatCharacterPill,
chatDrawerPersonaValue,
chatDiv,
chatList,
chatListLoading,
chatListLoadingText,
chatSidebar,
chatDrawerCloseBtn,
chatSearchInput,
chatUserPersonaPill,
clearChatBtn,
archiveChatBtn,
backupWorkspaceBtn,
clearUserPersonaBtn,
contextChipButton,
contextPopover,
contextSummaryLabel,
exportChatBtn,
moveChatFolderBtn,
loginForm,
@@ -42,9 +37,7 @@ import {
modelHelpBadge,
modelHelpSummary,
modelHelpTitle,
modelMenu,
modelMenuButton,
modelPopover,
modelSection,
modelSelect,
msgInput,
newChatBtn,
@@ -63,12 +56,9 @@ import {
personaForm,
personaFormNotice,
personaFormTitle,
personaList,
personaMenu,
personaMenuButton,
personaModal,
personaNameInput,
personaPopover,
personaSection,
personaPronounsInput,
personaTypeSelect,
personaAppearanceInput,
@@ -177,12 +167,8 @@ function applyWorkspaceMode(nextMode, persist = true) {
workspaceMode = nextMode === "advanced" ? "advanced" : "basic";
document.body.dataset.workspaceMode = workspaceMode;
if (persist) localStorage.setItem("krishd-workspace-mode", workspaceMode);
if (workspaceMode === "basic") {
closeModelPopover();
closePersonaPopover();
}
if (modelMenu) modelMenu.classList.toggle("hidden", workspaceMode === "basic");
if (personaMenu) personaMenu.classList.toggle("hidden", workspaceMode === "basic");
if (modelSection) modelSection.classList.toggle("hidden", workspaceMode === "basic");
if (personaSection) personaSection.classList.toggle("hidden", workspaceMode === "basic");
updateComposerPlaceholder();
}
@@ -658,7 +644,7 @@ function renderMessages() {
isNewest: index === currentMessages.length - 1
}));
}
updateChatParticipants();
renderDrawerSummary();
updateChatActionState();
updateComposerPlaceholder();
if (currentMessages.length) {
@@ -734,7 +720,7 @@ function updateWorkspaceCopy() {
if (sessionUser) {
sessionUser.textContent = currentUsername || "-";
}
updateChatParticipants();
renderDrawerSummary();
updateComposerPlaceholder();
}
@@ -770,13 +756,12 @@ function renderRoleplayStarterStep() {
}
}
function updateChatParticipants() {
function renderDrawerSummary() {
const chat = getChatById(activeChatId);
if (!chatCharacterPill || !chatUserPersonaPill) return;
if (!chat) {
chatCharacterPill.classList.add("hidden");
chatUserPersonaPill.classList.add("hidden");
if (contextSummaryLabel) contextSummaryLabel.textContent = "None";
return;
}
if (chat.assistant_persona_id) {
@@ -789,15 +774,14 @@ function updateChatParticipants() {
chatUserPersonaPill.textContent = "You as: yourself";
chatUserPersonaPill.classList.remove("hidden");
}
if (contextSummaryLabel) contextSummaryLabel.textContent = chat.assistant_persona_name || "Roleplay";
return;
}
chatCharacterPill.classList.add("hidden");
chatUserPersonaPill.classList.add("hidden");
if (contextSummaryLabel) contextSummaryLabel.textContent = "Assistant";
}
function updateContextRail() {
function renderModelSection() {
updateModelHelp();
return modelSelect?.selectedOptions[0]?.textContent || "";
}
@@ -871,7 +855,7 @@ function applyTheme(themeKey, persist = true) {
themeLogoTargets.forEach((target) => { target.textContent = theme.short; });
if (persist) localStorage.setItem("krishd-theme", nextTheme);
updateWorkspaceCopy();
updateContextRail();
renderModelSection();
}
function updateChatActionState() {
@@ -892,6 +876,15 @@ function updateChatActionState() {
if (backupWorkspaceBtn) backupWorkspaceBtn.disabled = !currentUsername;
}
function renderPersonaSection() {
updateActivePersonaStatus(userPersonas, activeUserPersonaId, activeUserPersonaStatus, "No default user persona set.", "Default user persona");
if (chatDrawerPersonaValue) {
const active = userPersonas.find((persona) => persona.id === activeUserPersonaId);
chatDrawerPersonaValue.textContent = active ? active.name : "None";
}
if (clearUserPersonaBtn) clearUserPersonaBtn.disabled = !activeUserPersonaId;
}
function maybeShowPersonaNudge() {
if (localStorage.getItem("krishd-persona-nudge-shown") === "true") return;
const assistantOnlyChats = chatSessions.filter((chat) => !chat.assistant_persona_id).length;
@@ -962,23 +955,13 @@ function renderChatList() {
}
function updateActivePersonaStatus(items, activeId, statusEl, emptyText, prefix) {
if (!statusEl) return;
const active = items.find((persona) => persona.id === activeId);
statusEl.textContent = active ? `${prefix}: ${active.name}` : emptyText;
}
function setActivePersonaStatus() {
if (activePersonaStatus) {
activePersonaStatus.textContent = assistantPersonas.length
? "Choose an AI Character, pair it with a user persona if needed, and start the scene."
: "No AI Characters yet. Create one to start a roleplay.";
}
updateActivePersonaStatus(userPersonas, activeUserPersonaId, activeUserPersonaStatus, "No default user persona set.", "Default user persona");
if (clearUserPersonaBtn) clearUserPersonaBtn.disabled = !activeUserPersonaId;
updateContextRail();
}
function openPersonaForm(persona = null, personaType = "assistant") {
closePersonaPopover();
closeChatDrawer();
closePopup(false);
personaModal.classList.remove("hidden");
if (persona) {
@@ -1001,28 +984,6 @@ function openPersonaForm(persona = null, personaType = "assistant") {
}
function closePersonaForm() { personaModal.classList.add("hidden"); editingPersonaId = null; editingPersonaType = "assistant"; personaTypeSelect.disabled = false; setPersonaFormNotice(""); }
function closePersonaPopover() {
if (!personaPopover || personaPopover.dataset.staticPanel === "true") return;
personaPopover.classList.add("hidden");
if (personaMenuButton) personaMenuButton.setAttribute("aria-expanded", "false");
}
function closeContextPopover() {
if (!contextPopover || !contextChipButton) return;
contextPopover.classList.add("hidden");
contextChipButton.setAttribute("aria-expanded", "false");
}
function closeChatActionsPopover() {
if (!chatActionsPopover || !chatActionsMenuButton) return;
chatActionsPopover.classList.add("hidden");
chatActionsMenuButton.setAttribute("aria-expanded", "false");
document.body.classList.remove("chat-drawer-open");
}
function closeAuxiliaryPopovers() {
closeModelPopover();
closePersonaPopover();
closeContextPopover();
closeChatActionsPopover();
}
function populateRoleplayStarter() {
if (!roleplayCharacterSelect || !roleplayUserPersonaSelect) return;
roleplayCharacterSelect.innerHTML = "";
@@ -1065,7 +1026,7 @@ function populateRoleplayStarter() {
renderRoleplayStarterStep();
}
function openRoleplayStarter(characterId = null) {
closeAuxiliaryPopovers();
closeChatDrawer();
roleplayPresetCharacterId = characterId;
roleplayStarterStep = 1;
populateRoleplayStarter();
@@ -1097,11 +1058,6 @@ function goToRoleplayStarterSceneStep() {
renderRoleplayStarterStep();
if (roleplayScenarioInput) roleplayScenarioInput.focus();
}
function closeModelPopover() {
if (!modelPopover || modelPopover.dataset.staticPanel === "true") return;
modelPopover.classList.add("hidden");
if (modelMenuButton) modelMenuButton.setAttribute("aria-expanded", "false");
}
function isPopupOpen() { return popupModal && !popupModal.classList.contains("hidden"); }
function finishPopup(result) {
if (typeof popupResolver === "function") popupResolver(result);
@@ -1160,7 +1116,7 @@ function promptPopup(options) {
function renderPersonaList(items, activeId, listElement, personaType) {
if (!listElement) {
setActivePersonaStatus();
renderPersonaSection();
return;
}
listElement.innerHTML = "";
@@ -1169,7 +1125,7 @@ function renderPersonaList(items, activeId, listElement, personaType) {
empty.className = "status persona-status";
empty.textContent = "No personas yet. Create one to get started.";
listElement.appendChild(empty);
setActivePersonaStatus();
renderPersonaSection();
return;
}
items.forEach((persona) => {
@@ -1192,7 +1148,7 @@ function renderPersonaList(items, activeId, listElement, personaType) {
}
titleRow.append(title, tag); item.append(titleRow, meta, actions); listElement.appendChild(item);
});
setActivePersonaStatus();
renderPersonaSection();
}
async function loadSummary() {
@@ -1207,7 +1163,7 @@ async function loadPersonas() {
activeUserPersonaId = res.activeUserPersonaId || null; publishedPersonaIds = new Set(res.publishedPersonaIds || []);
renderPersonaList(userPersonas, activeUserPersonaId, userPersonaList, "user");
populateRoleplayStarter();
setActivePersonaStatus();
renderPersonaSection();
updateWorkspaceCopy();
}
@@ -1533,8 +1489,7 @@ async function displayModels() {
const models = Array.isArray(res.models) ? res.models : null;
if (res.error || !models) {
modelSelect.innerHTML = "<option>Error loading models</option>";
updateModelHelp();
updateContextRail();
renderModelSection();
return setNotice("Models could not be loaded.", "error");
}
modelSelect.innerHTML = "";
@@ -1547,8 +1502,7 @@ async function displayModels() {
option.selected = preferredIndex >= 0 ? index === preferredIndex : index === 0;
modelSelect.appendChild(option);
});
updateModelHelp();
updateContextRail();
renderModelSection();
}
async function checkSession() {
@@ -1628,55 +1582,55 @@ async function sendMessage() {
}
}
function togglePersonaPopover(event) {
event.stopPropagation();
closeModelPopover();
closeContextPopover();
closeChatActionsPopover();
const isHidden = personaPopover.classList.contains("hidden");
personaPopover.classList.toggle("hidden", !isHidden);
personaMenuButton.setAttribute("aria-expanded", String(isHidden));
}
const chatActions = {
archive: async () => {
const chat = getChatById(activeChatId);
if (chat) await updateChatOrganization(chat.id, {archived: !chat.archived_at});
},
clear: async () => {
if (activeChatId) await clearChat(activeChatId);
},
export: () => {
exportCurrentChat();
},
folder: async () => {
const chat = getChatById(activeChatId);
if (!chat) return;
const folderName = await promptPopup({
eyebrow: "Folder",
title: "Move chat to folder",
description: "Leave blank to remove the folder.",
label: "Folder name",
value: chat.folder_name || "",
placeholder: "Research"
});
if (folderName === null) return;
await updateChatOrganization(chat.id, {folderName: String(folderName || "").trim() || null});
},
pin: async () => {
const chat = getChatById(activeChatId);
if (chat) await updateChatOrganization(chat.id, {isPinned: !chat.is_pinned});
},
rename: async () => {
if (activeChatId) await renameChat(activeChatId);
}
};
function toggleModelPopover(event) {
event.stopPropagation();
closePersonaPopover();
closeContextPopover();
closeChatActionsPopover();
const isHidden = modelPopover.classList.contains("hidden");
modelPopover.classList.toggle("hidden", !isHidden);
modelMenuButton.setAttribute("aria-expanded", String(isHidden));
}
function toggleContextPopover(event) {
if (!contextPopover || !contextChipButton) return;
event.stopPropagation();
closeModelPopover();
closePersonaPopover();
closeChatActionsPopover();
const isHidden = contextPopover.classList.contains("hidden");
contextPopover.classList.toggle("hidden", !isHidden);
contextChipButton.setAttribute("aria-expanded", String(isHidden));
}
function toggleChatActionsPopover(event) {
if (!chatActionsPopover || !chatActionsMenuButton) return;
event.stopPropagation();
closeModelPopover();
closePersonaPopover();
closeContextPopover();
if (isMobileLayout()) closeMobileSidebar();
const isHidden = chatActionsPopover.classList.contains("hidden");
chatActionsPopover.classList.toggle("hidden", !isHidden);
chatActionsMenuButton.setAttribute("aria-expanded", String(isHidden));
document.body.classList.toggle("chat-drawer-open", isHidden);
function bindChatDrawerActions() {
document.querySelectorAll("[data-chat-action]").forEach((button) => {
button.addEventListener("click", async () => {
closeChatDrawer();
const action = chatActions[button.dataset.chatAction];
if (typeof action === "function") await action();
});
});
}
toggleButtons.forEach((btn) => btn.addEventListener("click", () => showAuthScreen(btn.dataset.target)));
loginForm.addEventListener("submit", (event) => { event.preventDefault(); void handleAuth("login", {username: loginUsernameInput.value.trim(), password: loginPasswordInput.value.trim()}); });
registerForm.addEventListener("submit", (event) => { event.preventDefault(); void handleAuth("register", {username: registerUsernameInput.value.trim(), password: registerPasswordInput.value.trim()}); });
logoutButton.addEventListener("click", async () => {
closeChatActionsPopover();
closeChatDrawer();
await post("/logout", {}); chatDiv.style.display = "none"; authDiv.style.display = "grid";
activeChatId = null; currentUsername = ""; currentSummary = null; chatSessions = []; currentMessages = []; assistantPersonas = []; userPersonas = [];
messageRetryState = new Map();
@@ -1693,61 +1647,21 @@ msgInput.addEventListener("input", function () {
});
resizeComposerInput();
newChatBtn.addEventListener("click", () => {
closeChatActionsPopover();
closeChatDrawer();
closeMobileSidebar();
void createNewChat();
});
renameChatBtn.addEventListener("click", () => {
closeChatActionsPopover();
if (activeChatId) void renameChat(activeChatId);
});
clearChatBtn.addEventListener("click", () => {
closeChatActionsPopover();
if (activeChatId) void clearChat(activeChatId);
});
exportChatBtn.addEventListener("click", () => {
closeChatActionsPopover();
exportCurrentChat();
});
if (pinChatBtn) pinChatBtn.addEventListener("click", () => {
closeChatActionsPopover();
const chat = getChatById(activeChatId);
if (chat) void updateChatOrganization(chat.id, {isPinned: !chat.is_pinned});
});
if (moveChatFolderBtn) moveChatFolderBtn.addEventListener("click", async () => {
closeChatActionsPopover();
const chat = getChatById(activeChatId);
if (!chat) return;
const folderName = await promptPopup({
eyebrow: "Folder",
title: "Move chat to folder",
description: "Leave blank to remove the folder.",
label: "Folder name",
value: chat.folder_name || "",
placeholder: "Research"
});
if (folderName === null) return;
void updateChatOrganization(chat.id, {folderName: String(folderName || "").trim() || null});
});
if (archiveChatBtn) archiveChatBtn.addEventListener("click", () => {
closeChatActionsPopover();
const chat = getChatById(activeChatId);
if (chat) void updateChatOrganization(chat.id, {archived: !chat.archived_at});
});
if (backupWorkspaceBtn) backupWorkspaceBtn.addEventListener("click", () => {
closeChatActionsPopover();
closeChatDrawer();
void backupWorkspace();
});
chatSearchInput.addEventListener("input", () => { void loadChatSessions(); });
if (chatDrawerCloseBtn) chatDrawerCloseBtn.addEventListener("click", closeChatActionsPopover);
modelSelect.addEventListener("change", () => {
updateModelHelp();
updateContextRail();
closeModelPopover();
renderModelSection();
});
roleplayNewPersonaBtn.addEventListener("click", () => openPersonaForm(null, "assistant"));
clearUserPersonaBtn.addEventListener("click", () => {
closePersonaPopover();
closeChatDrawer();
void clearPersona("user");
});
personaForm.addEventListener("submit", (event) => { event.preventDefault(); void savePersona(); }); personaCloseBtn.addEventListener("click", closePersonaForm);
@@ -1787,22 +1701,7 @@ popupInput.addEventListener("keydown", (event) => {
closePopup(popupInput.value);
}
});
if (modelMenuButton && modelPopover) {
modelMenuButton.addEventListener("click", toggleModelPopover);
modelPopover.addEventListener("click", (event) => event.stopPropagation());
}
if (personaMenuButton && personaPopover) {
personaMenuButton.addEventListener("click", togglePersonaPopover);
personaPopover.addEventListener("click", (event) => event.stopPropagation());
}
if (contextChipButton && contextPopover) {
contextChipButton.addEventListener("click", toggleContextPopover);
contextPopover.addEventListener("click", (event) => event.stopPropagation());
}
if (chatActionsMenuButton && chatActionsPopover) {
chatActionsMenuButton.addEventListener("click", toggleChatActionsPopover);
chatActionsPopover.addEventListener("click", (event) => event.stopPropagation());
}
bindChatDrawerActions();
if (chatSidebar) {
chatSidebar.addEventListener("click", (event) => event.stopPropagation());
}
@@ -1827,7 +1726,6 @@ if (onboardingModal) {
});
}
document.addEventListener("click", () => {
closeAuxiliaryPopovers();
closeMobileSidebar();
});
window.addEventListener("resize", () => {
@@ -1835,7 +1733,6 @@ window.addEventListener("resize", () => {
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
closeAuxiliaryPopovers();
closeMobileSidebar();
closeRoleplayStarter();
closePersonaForm();
+177 -29
View File
@@ -1308,7 +1308,9 @@ select:focus {
}
.model-menu,
.persona-menu {
.chat-drawer-section--model,
.persona-menu,
.chat-drawer-section--persona {
position: relative;
}
@@ -1343,7 +1345,8 @@ select:focus {
border-color: var(--secondary-border-strong);
}
.model-popover {
.model-popover,
.model-panel {
position: absolute;
top: calc(100% + 10px);
right: 0;
@@ -1813,14 +1816,17 @@ select:focus {
}
.chat-header-actions .model-menu,
.chat-header-actions .chat-drawer-section--model,
.chat-header-actions .persona-menu,
.chat-header-actions .chat-drawer-section--persona,
.chat-header-actions .chat-actions-menu {
display: flex;
align-items: stretch;
align-self: stretch;
}
.persona-popover {
.persona-popover,
.persona-panel {
position: absolute;
top: calc(100% + 10px);
right: 0;
@@ -3034,7 +3040,8 @@ body[data-theme="beta-ai"] .persona-form .field {
max-width: 100%;
}
.persona-popover {
.persona-popover,
.persona-panel {
right: auto;
left: 0;
width: min(92vw, 560px);
@@ -3116,7 +3123,8 @@ body[data-theme="beta-ai"] .persona-form .field {
grid-template-columns: 1fr;
}
.model-popover {
.model-popover,
.model-panel {
left: 0;
right: auto;
width: min(92vw, 420px);
@@ -3243,7 +3251,9 @@ body[data-theme="beta-ai"] .persona-form .field {
}
.chat-header-actions .model-menu,
.chat-header-actions .chat-drawer-section--model,
.chat-header-actions .persona-menu,
.chat-header-actions .chat-drawer-section--persona,
.chat-header-actions .chat-actions-menu {
display: block;
align-self: auto;
@@ -3437,7 +3447,9 @@ body[data-theme="beta-ai"] .persona-form .field {
}
.chat-header-actions .model-menu,
.chat-header-actions .chat-drawer-section--model,
.chat-header-actions .persona-menu,
.chat-header-actions .chat-drawer-section--persona,
.chat-header-actions .chat-actions-menu {
display: block;
}
@@ -3547,7 +3559,9 @@ body[data-theme="beta-ai"] .persona-form .field {
}
.persona-popover,
.model-popover {
.persona-panel,
.model-popover,
.model-panel {
background: var(--panel-elevated);
border: 1px solid var(--line);
border-radius: 20px;
@@ -3556,15 +3570,18 @@ body[data-theme="beta-ai"] .persona-form .field {
gap: 18px;
}
.model-popover {
.model-popover,
.model-panel {
width: min(360px, 88vw);
}
.persona-popover {
.persona-popover,
.persona-panel {
width: min(420px, 88vw);
}
.chat-actions-menu .persona-popover {
.chat-actions-menu .persona-popover,
.chat-actions-menu .persona-panel {
width: min(520px, 92vw);
}
@@ -3749,7 +3766,9 @@ body[data-theme="beta-ai"] .persona-form .field {
}
.persona-popover,
.model-popover {
.persona-panel,
.model-popover,
.model-panel {
position: fixed;
left: 14px;
right: 14px;
@@ -3795,8 +3814,10 @@ body[data-theme="beta-ai"] .persona-form .field {
.chat-header-actions .secondary-action,
.chat-header-actions .model-badge-button,
.chat-header-actions .persona-menu,
.chat-header-actions .chat-drawer-section--persona,
.chat-header-actions .chat-actions-menu,
.chat-header-actions .model-menu {
.chat-header-actions .model-menu,
.chat-header-actions .chat-drawer-section--model {
width: 100%;
}
@@ -3962,7 +3983,26 @@ body[data-theme="beta-ai"] .persona-form .field {
}
.overflow-trigger {
min-width: 92px;
min-width: 0;
display: inline-flex;
align-items: center;
gap: 10px;
padding-inline: 12px 14px;
}
.overflow-trigger-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
min-width: 24px;
font-family: "Space Grotesk", sans-serif;
font-size: 1rem;
letter-spacing: 0.12em;
}
.overflow-trigger-label {
font-weight: 600;
}
.messages-panel,
@@ -4031,14 +4071,18 @@ body[data-theme="beta-ai"] .persona-form .field {
position: fixed;
inset: 0 0 0 auto;
width: min(560px, 100vw);
padding: 12px;
background: rgba(16, 24, 40, 0.18);
backdrop-filter: blur(10px);
padding: 10px;
background: rgba(16, 24, 40, 0.12);
backdrop-filter: blur(12px);
z-index: 50;
border-top-left-radius: 30px;
border-bottom-left-radius: 30px;
overflow: hidden;
}
.chat-drawer.hidden {
display: none;
border-radius: 30px;
}
.chat-drawer-panel {
@@ -4046,13 +4090,13 @@ body[data-theme="beta-ai"] .persona-form .field {
margin-left: auto;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
gap: 22px;
padding: 24px;
gap: 18px;
padding: 22px;
background: var(--panel-elevated);
border: 1px solid var(--line);
border-radius: 28px;
border-radius: 30px;
overflow: hidden;
box-shadow: 0 28px 80px rgba(0, 0, 0, 0.18);
box-shadow: 0 32px 88px rgba(0, 0, 0, 0.14);
}
.chat-drawer-header,
@@ -4074,21 +4118,21 @@ body[data-theme="beta-ai"] .persona-form .field {
overflow: auto;
overflow-x: hidden;
padding-inline: 2px 8px;
gap: 16px;
gap: 10px;
scrollbar-gutter: stable both-edges;
}
.chat-drawer-section {
padding: 18px;
background: var(--nav-item-surface);
border: 1px solid var(--pill-border);
border-radius: 22px;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.45);
padding: 14px 0 0;
background: transparent;
border: 0;
border-top: 1px solid var(--line);
border-radius: 0;
box-shadow: none;
}
.chat-drawer-section:last-child {
padding: 18px;
border-bottom: 1px solid var(--pill-border);
border-bottom: 0;
}
.chat-drawer .dropdown-header {
@@ -4104,14 +4148,114 @@ body[data-theme="beta-ai"] .persona-form .field {
max-width: 100%;
}
.chat-drawer-summary {
gap: 10px;
padding-bottom: 8px;
}
.chat-drawer-disclosure {
padding-top: 12px;
}
.chat-drawer-disclosure[open] {
gap: 14px;
}
.chat-drawer-disclosure > summary,
.chat-drawer-more > summary,
.chat-drawer-danger > summary {
list-style: none;
}
.chat-drawer-disclosure > summary::-webkit-details-marker,
.chat-drawer-more > summary::-webkit-details-marker,
.chat-drawer-danger > summary::-webkit-details-marker {
display: none;
}
.chat-drawer-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 16px;
align-items: center;
cursor: pointer;
}
.chat-drawer-row-label,
.chat-drawer-row-meta {
display: grid;
gap: 4px;
}
.chat-drawer-row-label strong,
.chat-drawer-row-meta strong {
font-size: 0.98rem;
line-height: 1.2;
}
.chat-drawer-row-meta {
justify-items: end;
text-align: right;
}
.chat-drawer-row-hint {
font-size: 0.75rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--text-muted);
}
.chat-drawer-disclosure[open] .chat-drawer-row-hint,
.chat-drawer-more[open] .chat-drawer-row-hint,
.chat-drawer-danger[open] .chat-drawer-row-hint {
color: var(--brand);
}
.chat-drawer-row--compact {
align-items: baseline;
}
.chat-drawer-disclosure-panel {
margin-top: 12px;
}
.chat-drawer-action-list {
display: grid;
gap: 10px;
}
.chat-drawer-action {
width: 100%;
justify-content: space-between;
text-align: left;
padding-inline: 16px;
border-radius: 16px;
background: var(--panel-soft);
border-color: var(--line);
}
.chat-drawer-action-danger {
color: var(--error);
border-color: rgba(181, 71, 61, 0.18);
background: rgba(181, 71, 61, 0.06);
}
.chat-drawer-danger {
border-top-color: rgba(181, 71, 61, 0.16);
}
.chat-drawer .model-menu,
.chat-drawer .persona-menu {
.chat-drawer .chat-drawer-section--model,
.chat-drawer .persona-menu,
.chat-drawer .chat-drawer-section--persona {
display: grid !important;
gap: 14px;
}
.chat-drawer .model-popover,
.chat-drawer .persona-popover {
.chat-drawer .model-panel,
.chat-drawer .persona-popover,
.chat-drawer .persona-panel {
position: static;
width: auto;
max-height: none;
@@ -4158,6 +4302,10 @@ body[data-theme="beta-ai"] .persona-form .field {
border-radius: 16px;
}
.chat-drawer .persona-section-header {
padding-inline: 2px;
}
.model-inline-badge {
display: inline-flex;
align-items: center;