implement more usability features

This commit is contained in:
2026-04-10 11:18:12 +02:00
parent ed9184a4d1
commit 1b57979f77
6 changed files with 496 additions and 137 deletions
+13
View File
@@ -211,6 +211,19 @@
</div>
</header>
<div id="chatActivityOverlay" class="chat-activity-overlay hidden" role="status" aria-live="polite" aria-atomic="true">
<div class="chat-activity-card">
<div class="chat-activity-mark" aria-hidden="true">
<span></span>
<span></span>
<span></span>
</div>
<p id="chatActivityEyebrow" class="eyebrow">Please wait</p>
<h3 id="chatActivityTitle">Preparing chat</h3>
<p id="chatActivityDetail" class="subtitle">The assistant is still working.</p>
</div>
</div>
<div id="messages" class="messages-panel messages-panel-focus"></div>
<div class="input-shell input-shell-focus">
+13
View File
@@ -169,6 +169,19 @@
</form>
</div>
</div>
<div id="marketActivityOverlay" class="chat-activity-overlay hidden" role="status" aria-live="polite" aria-atomic="true">
<div class="chat-activity-card">
<div class="chat-activity-mark" aria-hidden="true">
<span></span>
<span></span>
<span></span>
</div>
<p id="marketActivityEyebrow" class="eyebrow">Please wait</p>
<h3 id="marketActivityTitle">Opening roleplay</h3>
<p id="marketActivityDetail" class="subtitle">The AI character is preparing the first message.</p>
</div>
</div>
<script src="market.js" defer></script>
</body>
</html>
+34
View File
@@ -33,6 +33,10 @@ const marketPersonaAppearance = $("marketPersonaAppearance");
const marketPersonaBackground = $("marketPersonaBackground");
const marketPersonaDetails = $("marketPersonaDetails");
const marketPersonaExampleDialogues = $("marketPersonaExampleDialogues");
const marketActivityOverlay = $("marketActivityOverlay");
const marketActivityEyebrow = $("marketActivityEyebrow");
const marketActivityTitle = $("marketActivityTitle");
const marketActivityDetail = $("marketActivityDetail");
const themeNameTargets = document.querySelectorAll("[data-theme-name]");
const themes = {
@@ -51,6 +55,7 @@ let pendingMarketPersona = null;
let editingPersonaId = null;
let activeUserPersonaId = null;
let activeMarketView = "ai-characters";
let marketActivityDepth = 0;
async function request(url, data, method = "POST") {
try {
@@ -93,6 +98,29 @@ function setPersonaFormNotice(message = "", state = "") {
marketPersonaFormNotice.className = state ? `status ${state}` : "status";
}
function setMarketActivity(active, {
eyebrow = "Please wait",
title = "Opening roleplay",
detail = "The AI character is preparing the first message."
} = {}) {
if (!marketActivityOverlay || !marketActivityEyebrow || !marketActivityTitle || !marketActivityDetail) return;
if (active) {
marketActivityDepth += 1;
marketActivityEyebrow.textContent = eyebrow;
marketActivityTitle.textContent = title;
marketActivityDetail.textContent = detail;
marketActivityOverlay.classList.remove("hidden");
return;
}
marketActivityDepth = Math.max(0, marketActivityDepth - 1);
if (marketActivityDepth === 0) {
marketActivityOverlay.classList.add("hidden");
marketActivityEyebrow.textContent = "Please wait";
marketActivityTitle.textContent = "Opening roleplay";
marketActivityDetail.textContent = "The AI character is preparing the first message.";
}
}
function setMarketView(view) {
const normalizedView = view === "user-personas" ? "user-personas" : "ai-characters";
activeMarketView = normalizedView;
@@ -352,12 +380,18 @@ async function startMarketPersonaChat(marketId, userPersonaSelection, scenarioPr
}
setMarketStatus("Starting AI Character chat...");
setMarketActivity(true, {
eyebrow: "Starting roleplay",
title: "Opening AI Character chat",
detail: "Generating the initial message and preparing the chat window."
});
const userPersonaId = userPersonaSelection === "self" ? null : Number(userPersonaSelection);
const res = await post(`/personas/market/${marketId}/chat`, {
userPersonaId,
scenarioPrompt: scenarioPrompt.trim()
});
if (res.error || !res.chat) {
setMarketActivity(false);
setMarketStatus(res.error || "Unable to start chat.", "error");
return;
}
+90 -28
View File
@@ -13,6 +13,7 @@ const startRoleplayBtn = $("startRoleplay");
const chatListLoading = $("chatListLoading"), chatListLoadingText = $("chatListLoadingText");
const renameChatBtn = $("renameChat"), clearChatBtn = $("clearChat"), exportChatBtn = $("exportChat");
const activeChatTitle = $("activeChatTitle"), sessionUser = $("sessionUser");
const chatActivityOverlay = $("chatActivityOverlay"), chatActivityEyebrow = $("chatActivityEyebrow"), chatActivityTitle = $("chatActivityTitle"), chatActivityDetail = $("chatActivityDetail");
const contextChipButton = $("contextChipButton"), contextPopover = $("contextPopover"), contextSummaryLabel = $("contextSummaryLabel");
const chatActionsMenuButton = $("chatActionsMenuButton"), chatActionsPopover = $("chatActionsPopover");
const chatModePill = $("chatModePill"), chatCharacterPill = $("chatCharacterPill"), chatUserPersonaPill = $("chatUserPersonaPill"), chatScenePill = $("chatScenePill");
@@ -48,6 +49,7 @@ let noticeTimer = null;
let workspaceMode = localStorage.getItem("krishd-workspace-mode") || "basic";
let onboardingStep = 1, onboardingIntent = "ask";
let chatLoadingDepth = 0;
let chatActivityDepth = 0;
let messageRetryState = new Map();
const onboardingPrompts = {
@@ -131,6 +133,25 @@ function setChatLoading(active, message = "Loading chats...") {
}
}
function setChatActivity(active, {eyebrow = "Please wait", title = "Preparing chat", detail = "The assistant is still working."} = {}) {
if (!chatActivityOverlay || !chatActivityEyebrow || !chatActivityTitle || !chatActivityDetail) return;
if (active) {
chatActivityDepth += 1;
chatActivityEyebrow.textContent = eyebrow;
chatActivityTitle.textContent = title;
chatActivityDetail.textContent = detail;
chatActivityOverlay.classList.remove("hidden");
return;
}
chatActivityDepth = Math.max(0, chatActivityDepth - 1);
if (chatActivityDepth === 0) {
chatActivityOverlay.classList.add("hidden");
chatActivityEyebrow.textContent = "Please wait";
chatActivityTitle.textContent = "Preparing chat";
chatActivityDetail.textContent = "The assistant is still working.";
}
}
function applyWorkspaceMode(nextMode, persist = true) {
workspaceMode = nextMode === "advanced" ? "advanced" : "basic";
document.body.dataset.workspaceMode = workspaceMode;
@@ -351,17 +372,34 @@ function getPreviousUserMessageId(messageId) {
return null;
}
function getRetryStateFromMessage(message) {
const variants = Array.isArray(message?.retryVariants) && message.retryVariants.length
? [...message.retryVariants]
: [message?.content || ""];
const requestedActiveIndex = Number(message?.retryActiveIndex);
const activeIndex = Number.isInteger(requestedActiveIndex)
? Math.min(Math.max(requestedActiveIndex, 0), variants.length - 1)
: 0;
return {
variants,
activeIndex,
retriesUsed: Number(message?.retryRetriesUsed || 0),
promptMessageId: message?.retryPromptMessageId ?? getPreviousUserMessageId(message?.id)
};
}
function syncRetryStateFromMessage(message) {
if (!message?.id) return null;
const state = getRetryStateFromMessage(message);
messageRetryState.set(message.id, state);
return state;
}
function ensureRetryState(message) {
if (!message?.id) return null;
let state = messageRetryState.get(message.id);
if (!state) {
state = {
variants: [message.content || ""],
activeIndex: 0,
retriesUsed: 0,
promptMessageId: getPreviousUserMessageId(message.id)
};
messageRetryState.set(message.id, state);
state = syncRetryStateFromMessage(message);
}
return state;
}
@@ -386,12 +424,8 @@ async function editChatMessage(messageId, fallbackIndex = -1) {
if (res.error || !res.message) {
return setNotice(res.error || "Unable to edit message.", "error");
}
currentMessages[index].id = res.message.id || currentMessages[index].id;
currentMessages[index].content = res.message.content;
const retryState = message.id ? messageRetryState.get(message.id) : null;
if (retryState) {
retryState.variants[retryState.activeIndex] = res.message.content;
}
currentMessages[index] = {...currentMessages[index], ...res.message};
syncRetryStateFromMessage(currentMessages[index]);
renderMessages();
setNotice("Message updated.", "success");
}
@@ -438,7 +472,11 @@ async function retryLatestAssistantMessage(messageId, fallbackIndex = -1) {
if (!state || state.retriesUsed >= 5) {
return setNotice("Retry limit reached (5).", "error");
}
setLoadingState(true);
setLoadingState(true, {
eyebrow: "Regenerating",
title: "Generating another version",
detail: "The assistant is rewriting the latest reply."
});
setNotice("Regenerating reply...");
try {
const endpoint = getMessageEndpoint(target, "/retry");
@@ -447,19 +485,16 @@ async function retryLatestAssistantMessage(messageId, fallbackIndex = -1) {
if (res.error || !res.message?.content) {
return setNotice(res.error || "Unable to retry message.", "error");
}
state.retriesUsed += 1;
state.promptMessageId = res.promptMessageId || state.promptMessageId;
state.variants.push(res.message.content);
state.activeIndex = state.variants.length - 1;
const updateIndex = target.message?.id ? getMessageIndexById(target.message.id) : target.index;
if (updateIndex >= 0) {
currentMessages[updateIndex].id = res.message.id || currentMessages[updateIndex].id;
currentMessages[updateIndex].content = res.message.content;
currentMessages[updateIndex] = {...currentMessages[updateIndex], ...res.message};
syncRetryStateFromMessage(currentMessages[updateIndex]);
}
renderMessages();
setNotice(`Reply regenerated (${state.retriesUsed}/5).`, "success");
const nextState = res.message?.id ? messageRetryState.get(res.message.id) : state;
setNotice(`Reply regenerated (${nextState?.retriesUsed || state.retriesUsed}/5).`, "success");
} finally {
setLoadingState(false);
renderMessages();
}
}
@@ -478,15 +513,14 @@ async function switchRetryVariant(messageId, direction, fallbackIndex = -1) {
const targetContent = state.variants[nextIndex];
const endpoint = getMessageEndpoint(target);
if (!endpoint) return setNotice("Message is not ready yet. Try again.", "error");
const res = await put(endpoint, {content: targetContent});
const res = await put(endpoint, {content: targetContent, retryActiveIndex: nextIndex});
if (res.error || !res.message) {
return setNotice(res.error || "Unable to switch variant.", "error");
}
state.activeIndex = nextIndex;
const updateIndex = target.message?.id ? getMessageIndexById(target.message.id) : target.index;
if (updateIndex >= 0) {
currentMessages[updateIndex].id = res.message.id || currentMessages[updateIndex].id;
currentMessages[updateIndex].content = res.message.content;
currentMessages[updateIndex] = {...currentMessages[updateIndex], ...res.message};
syncRetryStateFromMessage(currentMessages[updateIndex]);
}
renderMessages();
}
@@ -1135,13 +1169,15 @@ async function deletePersona(id) {
async function publishPersona(id) { const res = await post(`/personas/${id}/publish`, {}); if (res.error) return setNotice(res.error, "error"); await loadPersonas(); setNotice("Persona published.", "success"); }
async function unpublishPersona(id) { const res = await post(`/personas/${id}/unpublish`, {}); if (res.error) return setNotice(res.error, "error"); await loadPersonas(); setNotice("Persona unpublished.", "success"); }
function setLoadingState(loading) {
function setLoadingState(loading, overlayOptions = null) {
isProcessing = loading;
msgInput.disabled = loading;
sendBtn.classList.toggle("loading", loading);
if (loading) {
msgInput.placeholder = "Processing response...";
setChatActivity(true, overlayOptions || undefined);
} else {
setChatActivity(false);
updateComposerPlaceholder();
}
updateSendState();
@@ -1155,12 +1191,18 @@ function showAuthScreen(target) {
async function setActiveChat(id) {
setChatLoading(true, "Opening chat...");
setChatActivity(true, {
eyebrow: "Opening chat",
title: "Loading conversation",
detail: "Fetching messages and restoring the current scene."
});
activeChatId = id;
messageRetryState = new Map();
updateWorkspaceCopy(); renderChatList(); updateChatActionState();
const res = await get(`/chats/${id}/messages`);
if (res.error) {
setChatLoading(false);
setChatActivity(false);
return setNotice(res.error, "error");
}
const existing = getChatById(id);
@@ -1168,6 +1210,7 @@ async function setActiveChat(id) {
currentMessages = res.messages || []; renderMessages();
updateSendState();
setChatLoading(false);
setChatActivity(false);
}
async function loadChatSessions() {
@@ -1194,23 +1237,36 @@ async function loadChatSessions() {
async function createNewChat() {
setChatLoading(true, "Creating chat...");
setChatActivity(true, {
eyebrow: "Creating chat",
title: "Opening a new conversation",
detail: "Setting up a fresh chat so you can start sending messages."
});
const res = await post("/chats", {title: "New chat"});
if (res.error || !res.chat) {
setChatLoading(false);
setChatActivity(false);
return setNotice(res.error || "Unable to create chat.", "error");
}
chatSessions.unshift(res.chat); await loadSummary(); renderChatList(); await setActiveChat(res.chat.id); setNotice("New chat created.", "success");
msgInput.focus();
setChatLoading(false);
setChatActivity(false);
}
async function startRoleplay() {
setChatLoading(true, "Starting roleplay...");
setChatActivity(true, {
eyebrow: "Starting roleplay",
title: "Building the opening scene",
detail: "Generating the first beat and opening the roleplay chat."
});
const assistantPersonaId = Number(roleplayCharacterSelect.value);
const userPersonaId = roleplayUserPersonaSelect.value ? Number(roleplayUserPersonaSelect.value) : null;
const scenarioPrompt = roleplayScenarioInput?.value.trim() || "";
if (!assistantPersonaId) {
setChatLoading(false);
setChatActivity(false);
return setRoleplayStarterNotice("Choose a character to start the roleplay.", "error");
}
roleplayStarterConfirm.disabled = true;
@@ -1224,6 +1280,7 @@ async function startRoleplay() {
roleplayStarterConfirm.disabled = false;
if (res.error || !res.chat) {
setChatLoading(false);
setChatActivity(false);
return setRoleplayStarterNotice(res.error || "Unable to start roleplay.", "error");
}
closeRoleplayStarter();
@@ -1231,6 +1288,7 @@ async function startRoleplay() {
await setActiveChat(res.chat.id);
setNotice(res.generatedInitialMessage ? "Roleplay started with a fresh scene." : "Roleplay reopened.", "success");
setChatLoading(false);
setChatActivity(false);
}
async function renameChat(id) {
@@ -1366,7 +1424,11 @@ async function sendMessage() {
currentChat.title = autoTitle; updateWorkspaceCopy(); renderChatList(); await put(`/chats/${activeChatId}`, {title: autoTitle});
}
currentMessages.push({role: "user", content: message}); addMessage(message, true); updateChatActionState();
msgInput.value = ""; msgInput.style.height = "auto"; setLoadingState(true); setNotice("Generating reply...");
msgInput.value = ""; msgInput.style.height = "auto"; setLoadingState(true, {
eyebrow: "Assistant replying",
title: "Generating response",
detail: "The assistant is reading your message and preparing a reply."
}); setNotice("Generating reply...");
const loadingMsg = addMessage("", false, true);
try {
const res = await post("/chat", {message, model: modelSelect.value, chatId: activeChatId});
+63
View File
@@ -811,6 +811,7 @@ select:focus {
gap: 16px;
min-height: 0;
overflow: hidden;
position: relative;
}
.chat-layout-focus {
@@ -831,6 +832,68 @@ select:focus {
min-height: 58vh;
}
.chat-activity-overlay {
position: fixed;
inset: 0;
z-index: 60;
display: grid;
place-items: center;
padding: 28px;
background:
linear-gradient(180deg, rgba(244, 239, 231, 0.78), rgba(248, 245, 239, 0.86));
backdrop-filter: blur(10px);
}
.chat-activity-card {
width: min(440px, 100%);
padding: 28px;
border-radius: 26px;
border: 1px solid rgba(24, 32, 31, 0.1);
background:
linear-gradient(155deg, rgba(255, 253, 249, 0.96), rgba(248, 241, 233, 0.94));
box-shadow: 0 24px 70px rgba(56, 39, 21, 0.16);
text-align: center;
}
.chat-activity-card h3 {
margin-top: 10px;
font-size: clamp(1.4rem, 2vw, 1.8rem);
}
.chat-activity-card .subtitle {
margin-top: 12px;
}
.chat-activity-mark {
display: inline-flex;
align-items: flex-end;
justify-content: center;
gap: 8px;
min-height: 34px;
margin-bottom: 10px;
}
.chat-activity-mark span {
width: 8px;
border-radius: 999px;
background: linear-gradient(180deg, var(--brand), var(--brand-strong));
animation: loadingWave 1.1s ease-in-out infinite;
}
.chat-activity-mark span:nth-child(1) {
height: 14px;
}
.chat-activity-mark span:nth-child(2) {
height: 28px;
animation-delay: 0.12s;
}
.chat-activity-mark span:nth-child(3) {
height: 18px;
animation-delay: 0.24s;
}
.input-shell-focus {
border-top: 1px solid rgba(24, 32, 31, 0.07);
}