fix more stuff and improve styling immensely

This commit is contained in:
2026-06-09 12:01:42 +02:00
parent f8d1c5370d
commit 58df321d8d
15 changed files with 1594 additions and 70 deletions
+14 -1
View File
@@ -2,6 +2,7 @@ export async function request(url, data, method = "POST") {
try {
const res = await fetch(url, {
method,
credentials: "same-origin",
headers: {"Content-Type": "application/json"},
body: data ? JSON.stringify(data) : undefined
});
@@ -19,12 +20,24 @@ export const del = (url) => request(url, null, "DELETE");
export async function stream(url, data, handlers = {}) {
const response = await fetch(url, {
method: "POST",
credentials: "same-origin",
headers: {"Content-Type": "application/json"},
body: JSON.stringify(data || {})
});
if (!response.ok || !response.body) {
throw new Error("Streaming request failed");
let error = "Streaming request failed";
try {
const payload = await response.json();
error = payload.error || error;
} catch {
// Keep the generic error if the server did not send JSON.
}
if (handlers.onError) {
handlers.onError({error});
return;
}
throw new Error(error);
}
const reader = response.body.getReader();
+52 -2
View File
@@ -1,11 +1,41 @@
import {chatDrawer, chatDrawerButton, chatDrawerCloseBtn} from "./dom.js";
let lastFocusedElement = null;
const focusableSelector = [
"a[href]",
"button:not([disabled])",
"textarea:not([disabled])",
"input:not([disabled])",
"select:not([disabled])",
"summary",
"[tabindex]:not([tabindex='-1'])"
].join(",");
function getFocusableElements() {
if (!chatDrawer) return [];
return Array.from(chatDrawer.querySelectorAll(focusableSelector))
.filter((element) => element instanceof HTMLElement && element.offsetParent !== null);
}
function syncChatDrawerState(isOpen) {
if (!chatDrawer || !chatDrawerButton) return;
if (isOpen) document.body.classList.remove("sidebar-open");
if (isOpen) {
lastFocusedElement = document.activeElement instanceof HTMLElement ? document.activeElement : null;
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);
if (isOpen) {
window.requestAnimationFrame(() => {
const firstFocusable = getFocusableElements()[0];
(chatDrawerCloseBtn || firstFocusable)?.focus();
});
} else if (lastFocusedElement) {
lastFocusedElement.focus();
lastFocusedElement = null;
}
}
function isChatDrawerOpen() {
@@ -34,7 +64,27 @@ function handleDocumentClick(event) {
}
function handleDocumentKeydown(event) {
if (event.key === "Escape") closeChatDrawer();
if (!isChatDrawerOpen()) return;
if (event.key === "Escape") {
closeChatDrawer();
return;
}
if (event.key !== "Tab") return;
const focusableElements = getFocusableElements();
if (!focusableElements.length) return;
const first = focusableElements[0];
const last = focusableElements[focusableElements.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
return;
}
if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
}
if (chatDrawerButton && chatDrawer) {
+2
View File
@@ -26,6 +26,7 @@ const registerSubmit = $("registerSubmit");
const chatList = $("chatList");
const chatSearchInput = $("chatSearch");
const chatSidebar = $("chatSidebar");
const chatSidebarToggle = $("chatSidebarToggle");
const chatDrawerCloseBtn = $("chatDrawerClose");
const chatDrawerButton = $("chatDrawerButton");
const chatDrawer = $("chatDrawer");
@@ -123,6 +124,7 @@ export {
chatListLoading,
chatListLoadingText,
chatSidebar,
chatSidebarToggle,
chatDrawerCloseBtn,
chatSearchInput,
chatUserPersonaPill,
+7 -5
View File
@@ -68,9 +68,9 @@
</div>
</section>
<section id="chat" class="card chat-card chat-focus-card" style="display:none;">
<section id="chat" class="card chat-card chat-focus-card" aria-label="AI chat workspace" style="display:none;">
<div class="chat-layout chat-layout-focus">
<aside id="chatSidebar" class="chat-sidebar">
<aside id="chatSidebar" class="chat-sidebar" aria-label="Workspace navigation">
<div class="sidebar-top">
<div class="sidebar-workspace-card">
<div>
@@ -100,8 +100,9 @@
</div>
</aside>
<section class="chat-main chat-main-focus">
<section class="chat-main chat-main-focus" aria-label="Active conversation">
<header class="chat-header chat-header-focus">
<button id="chatSidebarToggle" class="icon-button sidebar-toggle" type="button" aria-controls="chatSidebar" aria-expanded="false" aria-label="Open chat sidebar">Menu</button>
<div class="chat-title-block">
<div class="chat-title">
<h2 id="activeChatTitle">New chat</h2>
@@ -112,7 +113,7 @@
<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="chatDrawer" class="chat-drawer hidden" role="dialog" aria-modal="false" aria-labelledby="chatDrawerTitle">
<div id="chatDrawer" class="chat-drawer hidden" role="dialog" aria-modal="true" aria-labelledby="chatDrawerTitle">
<div class="chat-drawer-panel">
<div class="chat-drawer-header">
<div>
@@ -242,13 +243,14 @@
</div>
</div>
<div id="messages" class="messages-panel messages-panel-focus"></div>
<div id="messages" class="messages-panel messages-panel-focus" role="log" aria-live="polite" aria-relevant="additions text"></div>
<div class="input-shell input-shell-focus">
<div class="input-meta">
<p class="composer-hint">Enter to send · Shift+Enter for new line</p>
</div>
<div class="input-container">
<label class="visually-hidden" for="msgInput">Message</label>
<textarea id="msgInput" rows="1" placeholder="Type your message..."></textarea>
<button id="send" type="button" aria-label="Send message"></button>
</div>
+5 -5
View File
@@ -31,13 +31,13 @@
<p id="marketStatus" class="status persona-status">Loading market personas...</p>
<section class="settings-layout market-settings-layout">
<aside class="settings-nav" aria-label="Market sections">
<button class="settings-nav-item active" type="button" data-market-view="ai-characters">Characters</button>
<button class="settings-nav-item" type="button" data-market-view="user-personas">User personas</button>
<aside class="settings-nav" aria-label="Market sections" role="tablist">
<button class="settings-nav-item active" type="button" role="tab" aria-selected="true" data-market-view="ai-characters">Characters</button>
<button class="settings-nav-item" type="button" role="tab" aria-selected="false" data-market-view="user-personas">User personas</button>
</aside>
<div class="settings-content">
<section class="settings-view active" data-market-panel="ai-characters">
<section class="settings-view active" role="tabpanel" data-market-panel="ai-characters">
<div class="settings-panel">
<div class="settings-section-header">
<div>
@@ -63,7 +63,7 @@
</div>
</section>
<section class="settings-view" data-market-panel="user-personas">
<section class="settings-view" role="tabpanel" aria-hidden="true" data-market-panel="user-personas">
<div class="settings-panel">
<div class="settings-section-header">
<div>
+11 -2
View File
@@ -82,6 +82,7 @@ async function request(url, data, method = "POST") {
try {
const res = await fetch(url, {
method,
credentials: "same-origin",
headers: {"Content-Type": "application/json"},
body: data ? JSON.stringify(data) : undefined
});
@@ -183,8 +184,16 @@ function setMarketActivity(active, {
function setMarketView(view) {
const normalizedView = view === "user-personas" ? "user-personas" : "ai-characters";
activeMarketView = normalizedView;
marketViewButtons.forEach((item) => item.classList.toggle("active", item.dataset.marketView === normalizedView));
marketPanels.forEach((panel) => panel.classList.toggle("active", panel.dataset.marketPanel === normalizedView));
marketViewButtons.forEach((item) => {
const isActive = item.dataset.marketView === normalizedView;
item.classList.toggle("active", isActive);
item.setAttribute("aria-selected", String(isActive));
});
marketPanels.forEach((panel) => {
const isActive = panel.dataset.marketPanel === normalizedView;
panel.classList.toggle("active", isActive);
panel.setAttribute("aria-hidden", String(!isActive));
});
syncMarketQuery();
applyMarketFilter();
}
+51 -2
View File
@@ -18,6 +18,7 @@ import {
chatListLoading,
chatListLoadingText,
chatSidebar,
chatSidebarToggle,
chatSearchInput,
chatUserPersonaPill,
clearChatBtn,
@@ -179,6 +180,13 @@ function isMobileLayout() {
function closeMobileSidebar() {
if (!chatSidebar) return;
document.body.classList.remove("sidebar-open");
if (chatSidebarToggle) chatSidebarToggle.setAttribute("aria-expanded", "false");
}
function openMobileSidebar() {
if (!chatSidebar) return;
document.body.classList.add("sidebar-open");
if (chatSidebarToggle) chatSidebarToggle.setAttribute("aria-expanded", "true");
}
function hasCompletedOnboarding() {
@@ -541,7 +549,10 @@ async function switchRetryVariant(messageId, direction, fallbackIndex = -1) {
function addMessage(contentOrMessage, isUser = false, isLoading = false, options = {}) {
const msgDiv = document.createElement("div");
msgDiv.className = `msg ${isUser ? "user" : "bot"}${isLoading ? " loading" : ""}`;
msgDiv.setAttribute("role", "article");
msgDiv.tabIndex = 0;
if (isLoading) {
msgDiv.setAttribute("aria-label", "Assistant is responding");
msgDiv.innerHTML = `
<div class="loading-shell" aria-live="polite" aria-label="Assistant is responding">
<div class="loading-bars">
@@ -560,9 +571,11 @@ function addMessage(contentOrMessage, isUser = false, isLoading = false, options
: {id: null, role: isUser ? "user" : "bot", content: String(contentOrMessage || "")};
const isMessageUser = message.role === "user";
msgDiv.className = `msg ${isMessageUser ? "user" : "bot"}`;
msgDiv.setAttribute("aria-label", isMessageUser ? "Your message" : "Assistant message");
const header = document.createElement("div"), body = document.createElement("div"), actions = document.createElement("div");
header.className = "msg-header"; body.className = "msg-content";
actions.className = "msg-actions";
actions.setAttribute("aria-label", "Message actions");
const activeChat = getChatById(activeChatId);
header.textContent = isMessageUser ? "You" : (activeChat?.assistant_persona_name || "Assistant");
setMessageContent(body, message.content);
@@ -571,6 +584,7 @@ function addMessage(contentOrMessage, isUser = false, isLoading = false, options
editBtn.type = "button";
editBtn.className = "msg-action-btn";
editBtn.textContent = "Edit";
editBtn.setAttribute("aria-label", `Edit ${isMessageUser ? "your" : "assistant"} message`);
editBtn.addEventListener("click", () => {
void editChatMessage(message.id, options.messageIndex ?? -1);
});
@@ -580,6 +594,7 @@ function addMessage(contentOrMessage, isUser = false, isLoading = false, options
deleteBtn.type = "button";
deleteBtn.className = "msg-action-btn";
deleteBtn.textContent = "Delete";
deleteBtn.setAttribute("aria-label", `Delete ${isMessageUser ? "your" : "assistant"} message`);
deleteBtn.addEventListener("click", () => {
void deleteChatMessage(message.id, options.messageIndex ?? -1);
});
@@ -591,6 +606,7 @@ function addMessage(contentOrMessage, isUser = false, isLoading = false, options
retryBtn.type = "button";
retryBtn.className = "msg-action-btn";
retryBtn.textContent = `Retry (${state.retriesUsed}/5)`;
retryBtn.setAttribute("aria-label", `Retry assistant message, ${state.retriesUsed} of 5 retries used`);
retryBtn.disabled = state.retriesUsed >= 5 || isProcessing;
retryBtn.addEventListener("click", () => {
void retryLatestAssistantMessage(message.id, options.messageIndex ?? -1);
@@ -602,6 +618,7 @@ function addMessage(contentOrMessage, isUser = false, isLoading = false, options
prevBtn.type = "button";
prevBtn.className = "msg-action-btn";
prevBtn.textContent = "Prev";
prevBtn.setAttribute("aria-label", "Show previous retry variant");
prevBtn.disabled = state.activeIndex <= 0;
prevBtn.addEventListener("click", () => {
void switchRetryVariant(message.id, -1, options.messageIndex ?? -1);
@@ -611,6 +628,7 @@ function addMessage(contentOrMessage, isUser = false, isLoading = false, options
nextBtn.type = "button";
nextBtn.className = "msg-action-btn";
nextBtn.textContent = "Next";
nextBtn.setAttribute("aria-label", "Show next retry variant");
nextBtn.disabled = state.activeIndex >= state.variants.length - 1;
nextBtn.addEventListener("click", () => {
void switchRetryVariant(message.id, 1, options.messageIndex ?? -1);
@@ -935,10 +953,15 @@ function renderChatList() {
heading.className = "chat-list-section-header";
heading.textContent = group.label;
list.className = "chat-list-section-body";
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");
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";
title.textContent = chat.title;
meta.className = "chat-item-meta";
@@ -946,14 +969,25 @@ function renderChatList() {
? `Roleplay${chat.user_persona_name ? ` · ${chat.user_persona_name}` : ""}`
: "Assistant chat";
remove.type = "button";
remove.className = "chat-item-delete";
remove.textContent = "Delete";
remove.setAttribute("aria-label", `Delete chat ${chat.title}`);
remove.addEventListener("click", async (event) => {
event.stopPropagation();
await deleteChat(chat.id);
});
content.append(title, meta);
item.append(content, remove);
item.addEventListener("click", () => setActiveChat(chat.id));
item.addEventListener("click", () => {
closeMobileSidebar();
setActiveChat(chat.id);
});
item.addEventListener("keydown", (event) => {
if (event.key !== "Enter" && event.key !== " ") return;
event.preventDefault();
closeMobileSidebar();
void setActiveChat(chat.id);
});
list.appendChild(item);
});
chatList.appendChild(section);
@@ -1356,7 +1390,12 @@ async function startRoleplay() {
closeRoleplayStarter();
await loadChatSessions();
await setActiveChat(res.chat.id);
setNotice(res.generatedInitialMessage ? "Roleplay started with a fresh scene." : "Roleplay reopened.", "success");
setNotice(
res.degraded
? "Roleplay started, but the opening scene timed out. Send the first message to continue."
: res.generatedInitialMessage ? "Roleplay started with a fresh scene." : "Roleplay reopened.",
res.degraded ? "error" : "success"
);
setChatLoading(false);
setChatActivity(false);
}
@@ -1711,6 +1750,16 @@ bindChatDrawerActions();
if (chatSidebar) {
chatSidebar.addEventListener("click", (event) => event.stopPropagation());
}
if (chatSidebarToggle) {
chatSidebarToggle.addEventListener("click", (event) => {
event.stopPropagation();
if (document.body.classList.contains("sidebar-open")) {
closeMobileSidebar();
return;
}
openMobileSidebar();
});
}
if (onboardingContinue) {
onboardingContinue.addEventListener("click", () => {
onboardingStep = 2;
+7 -7
View File
@@ -25,14 +25,14 @@
<p id="settingsNotice" class="status">Loading settings...</p>
<section class="settings-layout">
<aside class="settings-nav" aria-label="Settings sections">
<button class="settings-nav-item active" type="button" data-settings-view="personal">Account</button>
<button class="settings-nav-item" type="button" data-settings-view="my-roleplay-companions">Personas</button>
<button class="settings-nav-item" type="button" data-settings-view="design">Appearance</button>
<aside class="settings-nav" aria-label="Settings sections" role="tablist">
<button class="settings-nav-item active" type="button" role="tab" aria-selected="true" data-settings-view="personal">Account</button>
<button class="settings-nav-item" type="button" role="tab" aria-selected="false" data-settings-view="my-roleplay-companions">Personas</button>
<button class="settings-nav-item" type="button" role="tab" aria-selected="false" data-settings-view="design">Appearance</button>
</aside>
<div class="settings-content">
<section class="settings-view active" data-settings-panel="personal">
<section class="settings-view active" role="tabpanel" data-settings-panel="personal">
<div class="settings-panel">
<div class="settings-section-header">
<div>
@@ -82,7 +82,7 @@
</div>
</section>
<section class="settings-view" data-settings-panel="my-roleplay-companions">
<section class="settings-view" role="tabpanel" aria-hidden="true" data-settings-panel="my-roleplay-companions">
<div class="settings-panel">
<div class="settings-section-header">
<div>
@@ -101,7 +101,7 @@
</div>
</section>
<section class="settings-view" data-settings-panel="design">
<section class="settings-view" role="tabpanel" aria-hidden="true" data-settings-panel="design">
<div class="settings-panel settings-panel-compact">
<div class="settings-section-header">
<div>
+11 -2
View File
@@ -61,6 +61,7 @@ async function request(url, data, method = "POST") {
try {
const res = await fetch(url, {
method,
credentials: "same-origin",
headers: {"Content-Type": "application/json"},
body: data ? JSON.stringify(data) : undefined
});
@@ -140,8 +141,16 @@ function applyThemeMode(modeKey, persist = true) {
function setSettingsView(view) {
const normalizedView = ["my-ai-characters", "my-personas"].includes(view) ? "my-roleplay-companions" : view;
settingsNavItems.forEach((item) => item.classList.toggle("active", item.dataset.settingsView === normalizedView));
settingsPanels.forEach((panel) => panel.classList.toggle("active", panel.dataset.settingsPanel === normalizedView));
settingsNavItems.forEach((item) => {
const isActive = item.dataset.settingsView === normalizedView;
item.classList.toggle("active", isActive);
item.setAttribute("aria-selected", String(isActive));
});
settingsPanels.forEach((panel) => {
const isActive = panel.dataset.settingsPanel === normalizedView;
panel.classList.toggle("active", isActive);
panel.setAttribute("aria-hidden", String(!isActive));
});
const params = new URLSearchParams(window.location.search);
params.set("view", normalizedView);
history.replaceState(null, "", `${window.location.pathname}?${params.toString()}`);
+1273
View File
File diff suppressed because it is too large Load Diff