implement more usability features
This commit is contained in:
@@ -211,6 +211,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</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 id="messages" class="messages-panel messages-panel-focus"></div>
|
||||||
|
|
||||||
<div class="input-shell input-shell-focus">
|
<div class="input-shell input-shell-focus">
|
||||||
|
|||||||
@@ -169,6 +169,19 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<script src="market.js" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ const marketPersonaAppearance = $("marketPersonaAppearance");
|
|||||||
const marketPersonaBackground = $("marketPersonaBackground");
|
const marketPersonaBackground = $("marketPersonaBackground");
|
||||||
const marketPersonaDetails = $("marketPersonaDetails");
|
const marketPersonaDetails = $("marketPersonaDetails");
|
||||||
const marketPersonaExampleDialogues = $("marketPersonaExampleDialogues");
|
const marketPersonaExampleDialogues = $("marketPersonaExampleDialogues");
|
||||||
|
const marketActivityOverlay = $("marketActivityOverlay");
|
||||||
|
const marketActivityEyebrow = $("marketActivityEyebrow");
|
||||||
|
const marketActivityTitle = $("marketActivityTitle");
|
||||||
|
const marketActivityDetail = $("marketActivityDetail");
|
||||||
const themeNameTargets = document.querySelectorAll("[data-theme-name]");
|
const themeNameTargets = document.querySelectorAll("[data-theme-name]");
|
||||||
|
|
||||||
const themes = {
|
const themes = {
|
||||||
@@ -51,6 +55,7 @@ let pendingMarketPersona = null;
|
|||||||
let editingPersonaId = null;
|
let editingPersonaId = null;
|
||||||
let activeUserPersonaId = null;
|
let activeUserPersonaId = null;
|
||||||
let activeMarketView = "ai-characters";
|
let activeMarketView = "ai-characters";
|
||||||
|
let marketActivityDepth = 0;
|
||||||
|
|
||||||
async function request(url, data, method = "POST") {
|
async function request(url, data, method = "POST") {
|
||||||
try {
|
try {
|
||||||
@@ -93,6 +98,29 @@ function setPersonaFormNotice(message = "", state = "") {
|
|||||||
marketPersonaFormNotice.className = state ? `status ${state}` : "status";
|
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) {
|
function setMarketView(view) {
|
||||||
const normalizedView = view === "user-personas" ? "user-personas" : "ai-characters";
|
const normalizedView = view === "user-personas" ? "user-personas" : "ai-characters";
|
||||||
activeMarketView = normalizedView;
|
activeMarketView = normalizedView;
|
||||||
@@ -352,12 +380,18 @@ async function startMarketPersonaChat(marketId, userPersonaSelection, scenarioPr
|
|||||||
}
|
}
|
||||||
|
|
||||||
setMarketStatus("Starting AI Character chat...");
|
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 userPersonaId = userPersonaSelection === "self" ? null : Number(userPersonaSelection);
|
||||||
const res = await post(`/personas/market/${marketId}/chat`, {
|
const res = await post(`/personas/market/${marketId}/chat`, {
|
||||||
userPersonaId,
|
userPersonaId,
|
||||||
scenarioPrompt: scenarioPrompt.trim()
|
scenarioPrompt: scenarioPrompt.trim()
|
||||||
});
|
});
|
||||||
if (res.error || !res.chat) {
|
if (res.error || !res.chat) {
|
||||||
|
setMarketActivity(false);
|
||||||
setMarketStatus(res.error || "Unable to start chat.", "error");
|
setMarketStatus(res.error || "Unable to start chat.", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
+90
-28
@@ -13,6 +13,7 @@ const startRoleplayBtn = $("startRoleplay");
|
|||||||
const chatListLoading = $("chatListLoading"), chatListLoadingText = $("chatListLoadingText");
|
const chatListLoading = $("chatListLoading"), chatListLoadingText = $("chatListLoadingText");
|
||||||
const renameChatBtn = $("renameChat"), clearChatBtn = $("clearChat"), exportChatBtn = $("exportChat");
|
const renameChatBtn = $("renameChat"), clearChatBtn = $("clearChat"), exportChatBtn = $("exportChat");
|
||||||
const activeChatTitle = $("activeChatTitle"), sessionUser = $("sessionUser");
|
const activeChatTitle = $("activeChatTitle"), sessionUser = $("sessionUser");
|
||||||
|
const chatActivityOverlay = $("chatActivityOverlay"), chatActivityEyebrow = $("chatActivityEyebrow"), chatActivityTitle = $("chatActivityTitle"), chatActivityDetail = $("chatActivityDetail");
|
||||||
const contextChipButton = $("contextChipButton"), contextPopover = $("contextPopover"), contextSummaryLabel = $("contextSummaryLabel");
|
const contextChipButton = $("contextChipButton"), contextPopover = $("contextPopover"), contextSummaryLabel = $("contextSummaryLabel");
|
||||||
const chatActionsMenuButton = $("chatActionsMenuButton"), chatActionsPopover = $("chatActionsPopover");
|
const chatActionsMenuButton = $("chatActionsMenuButton"), chatActionsPopover = $("chatActionsPopover");
|
||||||
const chatModePill = $("chatModePill"), chatCharacterPill = $("chatCharacterPill"), chatUserPersonaPill = $("chatUserPersonaPill"), chatScenePill = $("chatScenePill");
|
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 workspaceMode = localStorage.getItem("krishd-workspace-mode") || "basic";
|
||||||
let onboardingStep = 1, onboardingIntent = "ask";
|
let onboardingStep = 1, onboardingIntent = "ask";
|
||||||
let chatLoadingDepth = 0;
|
let chatLoadingDepth = 0;
|
||||||
|
let chatActivityDepth = 0;
|
||||||
let messageRetryState = new Map();
|
let messageRetryState = new Map();
|
||||||
|
|
||||||
const onboardingPrompts = {
|
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) {
|
function applyWorkspaceMode(nextMode, persist = true) {
|
||||||
workspaceMode = nextMode === "advanced" ? "advanced" : "basic";
|
workspaceMode = nextMode === "advanced" ? "advanced" : "basic";
|
||||||
document.body.dataset.workspaceMode = workspaceMode;
|
document.body.dataset.workspaceMode = workspaceMode;
|
||||||
@@ -351,17 +372,34 @@ function getPreviousUserMessageId(messageId) {
|
|||||||
return null;
|
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) {
|
function ensureRetryState(message) {
|
||||||
if (!message?.id) return null;
|
if (!message?.id) return null;
|
||||||
let state = messageRetryState.get(message.id);
|
let state = messageRetryState.get(message.id);
|
||||||
if (!state) {
|
if (!state) {
|
||||||
state = {
|
state = syncRetryStateFromMessage(message);
|
||||||
variants: [message.content || ""],
|
|
||||||
activeIndex: 0,
|
|
||||||
retriesUsed: 0,
|
|
||||||
promptMessageId: getPreviousUserMessageId(message.id)
|
|
||||||
};
|
|
||||||
messageRetryState.set(message.id, state);
|
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@@ -386,12 +424,8 @@ async function editChatMessage(messageId, fallbackIndex = -1) {
|
|||||||
if (res.error || !res.message) {
|
if (res.error || !res.message) {
|
||||||
return setNotice(res.error || "Unable to edit message.", "error");
|
return setNotice(res.error || "Unable to edit message.", "error");
|
||||||
}
|
}
|
||||||
currentMessages[index].id = res.message.id || currentMessages[index].id;
|
currentMessages[index] = {...currentMessages[index], ...res.message};
|
||||||
currentMessages[index].content = res.message.content;
|
syncRetryStateFromMessage(currentMessages[index]);
|
||||||
const retryState = message.id ? messageRetryState.get(message.id) : null;
|
|
||||||
if (retryState) {
|
|
||||||
retryState.variants[retryState.activeIndex] = res.message.content;
|
|
||||||
}
|
|
||||||
renderMessages();
|
renderMessages();
|
||||||
setNotice("Message updated.", "success");
|
setNotice("Message updated.", "success");
|
||||||
}
|
}
|
||||||
@@ -438,7 +472,11 @@ async function retryLatestAssistantMessage(messageId, fallbackIndex = -1) {
|
|||||||
if (!state || state.retriesUsed >= 5) {
|
if (!state || state.retriesUsed >= 5) {
|
||||||
return setNotice("Retry limit reached (5).", "error");
|
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...");
|
setNotice("Regenerating reply...");
|
||||||
try {
|
try {
|
||||||
const endpoint = getMessageEndpoint(target, "/retry");
|
const endpoint = getMessageEndpoint(target, "/retry");
|
||||||
@@ -447,19 +485,16 @@ async function retryLatestAssistantMessage(messageId, fallbackIndex = -1) {
|
|||||||
if (res.error || !res.message?.content) {
|
if (res.error || !res.message?.content) {
|
||||||
return setNotice(res.error || "Unable to retry message.", "error");
|
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;
|
const updateIndex = target.message?.id ? getMessageIndexById(target.message.id) : target.index;
|
||||||
if (updateIndex >= 0) {
|
if (updateIndex >= 0) {
|
||||||
currentMessages[updateIndex].id = res.message.id || currentMessages[updateIndex].id;
|
currentMessages[updateIndex] = {...currentMessages[updateIndex], ...res.message};
|
||||||
currentMessages[updateIndex].content = res.message.content;
|
syncRetryStateFromMessage(currentMessages[updateIndex]);
|
||||||
}
|
}
|
||||||
renderMessages();
|
const nextState = res.message?.id ? messageRetryState.get(res.message.id) : state;
|
||||||
setNotice(`Reply regenerated (${state.retriesUsed}/5).`, "success");
|
setNotice(`Reply regenerated (${nextState?.retriesUsed || state.retriesUsed}/5).`, "success");
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingState(false);
|
setLoadingState(false);
|
||||||
|
renderMessages();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,15 +513,14 @@ async function switchRetryVariant(messageId, direction, fallbackIndex = -1) {
|
|||||||
const targetContent = state.variants[nextIndex];
|
const targetContent = state.variants[nextIndex];
|
||||||
const endpoint = getMessageEndpoint(target);
|
const endpoint = getMessageEndpoint(target);
|
||||||
if (!endpoint) return setNotice("Message is not ready yet. Try again.", "error");
|
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) {
|
if (res.error || !res.message) {
|
||||||
return setNotice(res.error || "Unable to switch variant.", "error");
|
return setNotice(res.error || "Unable to switch variant.", "error");
|
||||||
}
|
}
|
||||||
state.activeIndex = nextIndex;
|
|
||||||
const updateIndex = target.message?.id ? getMessageIndexById(target.message.id) : target.index;
|
const updateIndex = target.message?.id ? getMessageIndexById(target.message.id) : target.index;
|
||||||
if (updateIndex >= 0) {
|
if (updateIndex >= 0) {
|
||||||
currentMessages[updateIndex].id = res.message.id || currentMessages[updateIndex].id;
|
currentMessages[updateIndex] = {...currentMessages[updateIndex], ...res.message};
|
||||||
currentMessages[updateIndex].content = res.message.content;
|
syncRetryStateFromMessage(currentMessages[updateIndex]);
|
||||||
}
|
}
|
||||||
renderMessages();
|
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 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"); }
|
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;
|
isProcessing = loading;
|
||||||
msgInput.disabled = loading;
|
msgInput.disabled = loading;
|
||||||
sendBtn.classList.toggle("loading", loading);
|
sendBtn.classList.toggle("loading", loading);
|
||||||
if (loading) {
|
if (loading) {
|
||||||
msgInput.placeholder = "Processing response...";
|
msgInput.placeholder = "Processing response...";
|
||||||
|
setChatActivity(true, overlayOptions || undefined);
|
||||||
} else {
|
} else {
|
||||||
|
setChatActivity(false);
|
||||||
updateComposerPlaceholder();
|
updateComposerPlaceholder();
|
||||||
}
|
}
|
||||||
updateSendState();
|
updateSendState();
|
||||||
@@ -1155,12 +1191,18 @@ function showAuthScreen(target) {
|
|||||||
|
|
||||||
async function setActiveChat(id) {
|
async function setActiveChat(id) {
|
||||||
setChatLoading(true, "Opening chat...");
|
setChatLoading(true, "Opening chat...");
|
||||||
|
setChatActivity(true, {
|
||||||
|
eyebrow: "Opening chat",
|
||||||
|
title: "Loading conversation",
|
||||||
|
detail: "Fetching messages and restoring the current scene."
|
||||||
|
});
|
||||||
activeChatId = id;
|
activeChatId = id;
|
||||||
messageRetryState = new Map();
|
messageRetryState = new Map();
|
||||||
updateWorkspaceCopy(); renderChatList(); updateChatActionState();
|
updateWorkspaceCopy(); renderChatList(); updateChatActionState();
|
||||||
const res = await get(`/chats/${id}/messages`);
|
const res = await get(`/chats/${id}/messages`);
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
setChatLoading(false);
|
setChatLoading(false);
|
||||||
|
setChatActivity(false);
|
||||||
return setNotice(res.error, "error");
|
return setNotice(res.error, "error");
|
||||||
}
|
}
|
||||||
const existing = getChatById(id);
|
const existing = getChatById(id);
|
||||||
@@ -1168,6 +1210,7 @@ async function setActiveChat(id) {
|
|||||||
currentMessages = res.messages || []; renderMessages();
|
currentMessages = res.messages || []; renderMessages();
|
||||||
updateSendState();
|
updateSendState();
|
||||||
setChatLoading(false);
|
setChatLoading(false);
|
||||||
|
setChatActivity(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadChatSessions() {
|
async function loadChatSessions() {
|
||||||
@@ -1194,23 +1237,36 @@ async function loadChatSessions() {
|
|||||||
|
|
||||||
async function createNewChat() {
|
async function createNewChat() {
|
||||||
setChatLoading(true, "Creating chat...");
|
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"});
|
const res = await post("/chats", {title: "New chat"});
|
||||||
if (res.error || !res.chat) {
|
if (res.error || !res.chat) {
|
||||||
setChatLoading(false);
|
setChatLoading(false);
|
||||||
|
setChatActivity(false);
|
||||||
return setNotice(res.error || "Unable to create chat.", "error");
|
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");
|
chatSessions.unshift(res.chat); await loadSummary(); renderChatList(); await setActiveChat(res.chat.id); setNotice("New chat created.", "success");
|
||||||
msgInput.focus();
|
msgInput.focus();
|
||||||
setChatLoading(false);
|
setChatLoading(false);
|
||||||
|
setChatActivity(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startRoleplay() {
|
async function startRoleplay() {
|
||||||
setChatLoading(true, "Starting roleplay...");
|
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 assistantPersonaId = Number(roleplayCharacterSelect.value);
|
||||||
const userPersonaId = roleplayUserPersonaSelect.value ? Number(roleplayUserPersonaSelect.value) : null;
|
const userPersonaId = roleplayUserPersonaSelect.value ? Number(roleplayUserPersonaSelect.value) : null;
|
||||||
const scenarioPrompt = roleplayScenarioInput?.value.trim() || "";
|
const scenarioPrompt = roleplayScenarioInput?.value.trim() || "";
|
||||||
if (!assistantPersonaId) {
|
if (!assistantPersonaId) {
|
||||||
setChatLoading(false);
|
setChatLoading(false);
|
||||||
|
setChatActivity(false);
|
||||||
return setRoleplayStarterNotice("Choose a character to start the roleplay.", "error");
|
return setRoleplayStarterNotice("Choose a character to start the roleplay.", "error");
|
||||||
}
|
}
|
||||||
roleplayStarterConfirm.disabled = true;
|
roleplayStarterConfirm.disabled = true;
|
||||||
@@ -1224,6 +1280,7 @@ async function startRoleplay() {
|
|||||||
roleplayStarterConfirm.disabled = false;
|
roleplayStarterConfirm.disabled = false;
|
||||||
if (res.error || !res.chat) {
|
if (res.error || !res.chat) {
|
||||||
setChatLoading(false);
|
setChatLoading(false);
|
||||||
|
setChatActivity(false);
|
||||||
return setRoleplayStarterNotice(res.error || "Unable to start roleplay.", "error");
|
return setRoleplayStarterNotice(res.error || "Unable to start roleplay.", "error");
|
||||||
}
|
}
|
||||||
closeRoleplayStarter();
|
closeRoleplayStarter();
|
||||||
@@ -1231,6 +1288,7 @@ async function startRoleplay() {
|
|||||||
await setActiveChat(res.chat.id);
|
await setActiveChat(res.chat.id);
|
||||||
setNotice(res.generatedInitialMessage ? "Roleplay started with a fresh scene." : "Roleplay reopened.", "success");
|
setNotice(res.generatedInitialMessage ? "Roleplay started with a fresh scene." : "Roleplay reopened.", "success");
|
||||||
setChatLoading(false);
|
setChatLoading(false);
|
||||||
|
setChatActivity(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renameChat(id) {
|
async function renameChat(id) {
|
||||||
@@ -1366,7 +1424,11 @@ async function sendMessage() {
|
|||||||
currentChat.title = autoTitle; updateWorkspaceCopy(); renderChatList(); await put(`/chats/${activeChatId}`, {title: autoTitle});
|
currentChat.title = autoTitle; updateWorkspaceCopy(); renderChatList(); await put(`/chats/${activeChatId}`, {title: autoTitle});
|
||||||
}
|
}
|
||||||
currentMessages.push({role: "user", content: message}); addMessage(message, true); updateChatActionState();
|
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);
|
const loadingMsg = addMessage("", false, true);
|
||||||
try {
|
try {
|
||||||
const res = await post("/chat", {message, model: modelSelect.value, chatId: activeChatId});
|
const res = await post("/chat", {message, model: modelSelect.value, chatId: activeChatId});
|
||||||
|
|||||||
@@ -811,6 +811,7 @@ select:focus {
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-layout-focus {
|
.chat-layout-focus {
|
||||||
@@ -831,6 +832,68 @@ select:focus {
|
|||||||
min-height: 58vh;
|
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 {
|
.input-shell-focus {
|
||||||
border-top: 1px solid rgba(24, 32, 31, 0.07);
|
border-top: 1px solid rgba(24, 32, 31, 0.07);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -185,6 +185,16 @@ db.prepare(`
|
|||||||
TEXT
|
TEXT
|
||||||
NOT
|
NOT
|
||||||
NULL,
|
NULL,
|
||||||
|
retry_variants
|
||||||
|
TEXT,
|
||||||
|
retry_active_index
|
||||||
|
INTEGER
|
||||||
|
DEFAULT 0,
|
||||||
|
retry_retries_used
|
||||||
|
INTEGER
|
||||||
|
DEFAULT 0,
|
||||||
|
retry_prompt_message_id
|
||||||
|
INTEGER,
|
||||||
created_at
|
created_at
|
||||||
DATETIME
|
DATETIME
|
||||||
DEFAULT
|
DEFAULT
|
||||||
@@ -444,11 +454,11 @@ const clearUserPersonaFromChatsStmt = db.prepare(`
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
const insertChatMessageStmt = db.prepare(
|
const insertChatMessageStmt = db.prepare(
|
||||||
"INSERT INTO chat_messages (chat_id, role, content) VALUES (?, ?, ?)"
|
"INSERT INTO chat_messages (chat_id, role, content, retry_variants, retry_active_index, retry_retries_used, retry_prompt_message_id) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
||||||
);
|
);
|
||||||
|
|
||||||
const getChatMessagesStmt = db.prepare(`
|
const getChatMessagesStmt = db.prepare(`
|
||||||
SELECT id, role, content
|
SELECT id, role, content, retry_variants, retry_active_index, retry_retries_used, retry_prompt_message_id
|
||||||
FROM chat_messages
|
FROM chat_messages
|
||||||
WHERE chat_id = ?
|
WHERE chat_id = ?
|
||||||
ORDER BY id
|
ORDER BY id
|
||||||
@@ -477,7 +487,7 @@ const getRecentChatMessagesUpToIdStmt = db.prepare(`
|
|||||||
;
|
;
|
||||||
|
|
||||||
const getChatMessageByIdStmt = db.prepare(`
|
const getChatMessageByIdStmt = db.prepare(`
|
||||||
SELECT id, role, content
|
SELECT id, role, content, retry_variants, retry_active_index, retry_retries_used, retry_prompt_message_id
|
||||||
FROM chat_messages
|
FROM chat_messages
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
AND chat_id = ?
|
AND chat_id = ?
|
||||||
@@ -486,7 +496,7 @@ const getChatMessageByIdStmt = db.prepare(`
|
|||||||
;
|
;
|
||||||
|
|
||||||
const getChatMessageByIndexStmt = db.prepare(`
|
const getChatMessageByIndexStmt = db.prepare(`
|
||||||
SELECT id, role, content
|
SELECT id, role, content, retry_variants, retry_active_index, retry_retries_used, retry_prompt_message_id
|
||||||
FROM chat_messages
|
FROM chat_messages
|
||||||
WHERE chat_id = ?
|
WHERE chat_id = ?
|
||||||
ORDER BY id
|
ORDER BY id
|
||||||
@@ -496,7 +506,7 @@ const getChatMessageByIndexStmt = db.prepare(`
|
|||||||
;
|
;
|
||||||
|
|
||||||
const getLatestChatMessageStmt = db.prepare(`
|
const getLatestChatMessageStmt = db.prepare(`
|
||||||
SELECT id, role, content
|
SELECT id, role, content, retry_variants, retry_active_index, retry_retries_used, retry_prompt_message_id
|
||||||
FROM chat_messages
|
FROM chat_messages
|
||||||
WHERE chat_id = ?
|
WHERE chat_id = ?
|
||||||
ORDER BY id DESC
|
ORDER BY id DESC
|
||||||
@@ -506,7 +516,7 @@ const getLatestChatMessageStmt = db.prepare(`
|
|||||||
;
|
;
|
||||||
|
|
||||||
const getPreviousUserMessageStmt = db.prepare(`
|
const getPreviousUserMessageStmt = db.prepare(`
|
||||||
SELECT id, role, content
|
SELECT id, role, content, retry_variants, retry_active_index, retry_retries_used, retry_prompt_message_id
|
||||||
FROM chat_messages
|
FROM chat_messages
|
||||||
WHERE chat_id = ?
|
WHERE chat_id = ?
|
||||||
AND role = 'user'
|
AND role = 'user'
|
||||||
@@ -521,6 +531,10 @@ const updateChatMessageStmt = db.prepare(
|
|||||||
"UPDATE chat_messages SET content = ? WHERE id = ? AND chat_id = ?"
|
"UPDATE chat_messages SET content = ? WHERE id = ? AND chat_id = ?"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const updateChatMessageWithRetryStateStmt = db.prepare(
|
||||||
|
"UPDATE chat_messages SET content = ?, retry_variants = ?, retry_active_index = ?, retry_retries_used = ?, retry_prompt_message_id = ? WHERE id = ? AND chat_id = ?"
|
||||||
|
);
|
||||||
|
|
||||||
const deleteChatMessageStmt = db.prepare(
|
const deleteChatMessageStmt = db.prepare(
|
||||||
"DELETE FROM chat_messages WHERE id = ? AND chat_id = ?"
|
"DELETE FROM chat_messages WHERE id = ? AND chat_id = ?"
|
||||||
);
|
);
|
||||||
@@ -810,7 +824,7 @@ if (legacyChatsExist && migrationNeeded.count === 0) {
|
|||||||
const sessionId = insertChatSessionStmt.run(username, "Imported chat", null, null).lastInsertRowid;
|
const sessionId = insertChatSessionStmt.run(username, "Imported chat", null, null).lastInsertRowid;
|
||||||
const legacyMessages = getLegacyChatsStmt.all(username);
|
const legacyMessages = getLegacyChatsStmt.all(username);
|
||||||
legacyMessages.forEach(msg => {
|
legacyMessages.forEach(msg => {
|
||||||
insertChatMessageStmt.run(sessionId, msg.role, msg.content);
|
insertChatMessage(sessionId, msg.role, msg.content);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -858,6 +872,26 @@ if (!hasMarketExampleDialogues) {
|
|||||||
}
|
}
|
||||||
db.prepare("UPDATE persona_market SET usage_count = 0 WHERE usage_count IS NULL").run();
|
db.prepare("UPDATE persona_market SET usage_count = 0 WHERE usage_count IS NULL").run();
|
||||||
|
|
||||||
|
const chatMessageColumns = db.prepare("PRAGMA table_info(chat_messages)").all();
|
||||||
|
const hasRetryVariants = chatMessageColumns.some(column => column.name === "retry_variants");
|
||||||
|
if (!hasRetryVariants) {
|
||||||
|
db.prepare("ALTER TABLE chat_messages ADD COLUMN retry_variants TEXT").run();
|
||||||
|
}
|
||||||
|
const hasRetryActiveIndex = chatMessageColumns.some(column => column.name === "retry_active_index");
|
||||||
|
if (!hasRetryActiveIndex) {
|
||||||
|
db.prepare("ALTER TABLE chat_messages ADD COLUMN retry_active_index INTEGER DEFAULT 0").run();
|
||||||
|
}
|
||||||
|
const hasRetryRetriesUsed = chatMessageColumns.some(column => column.name === "retry_retries_used");
|
||||||
|
if (!hasRetryRetriesUsed) {
|
||||||
|
db.prepare("ALTER TABLE chat_messages ADD COLUMN retry_retries_used INTEGER DEFAULT 0").run();
|
||||||
|
}
|
||||||
|
const hasRetryPromptMessageId = chatMessageColumns.some(column => column.name === "retry_prompt_message_id");
|
||||||
|
if (!hasRetryPromptMessageId) {
|
||||||
|
db.prepare("ALTER TABLE chat_messages ADD COLUMN retry_prompt_message_id INTEGER").run();
|
||||||
|
}
|
||||||
|
db.prepare("UPDATE chat_messages SET retry_active_index = 0 WHERE retry_active_index IS NULL").run();
|
||||||
|
db.prepare("UPDATE chat_messages SET retry_retries_used = 0 WHERE retry_retries_used IS NULL").run();
|
||||||
|
|
||||||
const chatSessionColumns = db.prepare("PRAGMA table_info(chat_sessions)").all();
|
const chatSessionColumns = db.prepare("PRAGMA table_info(chat_sessions)").all();
|
||||||
const hasAssistantPersonaLink = chatSessionColumns.some(column => column.name === "assistant_persona_id");
|
const hasAssistantPersonaLink = chatSessionColumns.some(column => column.name === "assistant_persona_id");
|
||||||
if (!hasAssistantPersonaLink) {
|
if (!hasAssistantPersonaLink) {
|
||||||
@@ -1132,6 +1166,10 @@ const buildRoleplayDirectionPrompt = ({assistantPersona, userPersona, scenarioPr
|
|||||||
"Ground the interaction in a specific scenario, place, and immediate dramatic situation.",
|
"Ground the interaction in a specific scenario, place, and immediate dramatic situation.",
|
||||||
"Make the assistant's first instinct, language, and priorities fit their persona exactly.",
|
"Make the assistant's first instinct, language, and priorities fit their persona exactly.",
|
||||||
"Use the user persona as interaction context so the assistant addresses them in a fitting way.",
|
"Use the user persona as interaction context so the assistant addresses them in a fitting way.",
|
||||||
|
"Format scene description, body language, and nonverbal actions in italics using *word*.",
|
||||||
|
"Format direct speech in double quotes like \"word\".",
|
||||||
|
"Format shouted words, sharp emphasis, or explosive expressions in bold using **word**.",
|
||||||
|
"Keep formatting readable and intentional instead of wrapping every sentence in markdown.",
|
||||||
"Avoid generic openings, meta commentary, and requests for permission to begin.",
|
"Avoid generic openings, meta commentary, and requests for permission to begin.",
|
||||||
"Do not write dialogue, internal thoughts, choices, or actions for the user.",
|
"Do not write dialogue, internal thoughts, choices, or actions for the user.",
|
||||||
"Never output any lines like 'User:' or 'You:' and never narrate what the user does.",
|
"Never output any lines like 'User:' or 'You:' and never narrate what the user does.",
|
||||||
@@ -1166,6 +1204,9 @@ const buildRoleplayOpenerPrompt = ({assistantPersona, userPersona, scenarioPromp
|
|||||||
`Address ${userName} naturally within the scene.`,
|
`Address ${userName} naturally within the scene.`,
|
||||||
"The message should establish the situation immediately instead of explaining setup out of character.",
|
"The message should establish the situation immediately instead of explaining setup out of character.",
|
||||||
"Use 1 to 3 short paragraphs. Sensory detail is allowed, but keep momentum.",
|
"Use 1 to 3 short paragraphs. Sensory detail is allowed, but keep momentum.",
|
||||||
|
"Write scene description, body language, and nonverbal actions in italics using *word*.",
|
||||||
|
"Write direct speech in double quotes like \"word\".",
|
||||||
|
"Use bold markdown like **word** only for loud, forceful, or strongly emphasized expressions.",
|
||||||
"Write only the assistant character's words and optional self-actions.",
|
"Write only the assistant character's words and optional self-actions.",
|
||||||
"Do not write any user dialogue, thoughts, feelings, choices, or actions.",
|
"Do not write any user dialogue, thoughts, feelings, choices, or actions.",
|
||||||
"Do not use labels such as 'User:' or 'You:'.",
|
"Do not use labels such as 'User:' or 'You:'.",
|
||||||
@@ -1221,6 +1262,139 @@ const generateRoleplayOpener = async ({selectedModel = "mistral:latest", assista
|
|||||||
return opener;
|
return opener;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const buildRetryPayload = async ({chatId, user, session, selectedModel, targetMessage}) => {
|
||||||
|
const activePersona = getAssistantPersonaForChatStmt.get(chatId, user);
|
||||||
|
const activeUserPersona = getUserPersonaForChatStmt.get(chatId, user) || getActiveUserPersonaStmt.get(user);
|
||||||
|
const sceneSummary = session.scenario_summary;
|
||||||
|
const scenarioPrompt = session.scenario_prompt;
|
||||||
|
const promptMessage = getPreviousUserMessageStmt.get(chatId, targetMessage.id);
|
||||||
|
|
||||||
|
if (!promptMessage) {
|
||||||
|
if (!activePersona || !sceneSummary) {
|
||||||
|
return {error: "No previous user prompt found for retry"};
|
||||||
|
}
|
||||||
|
const opener = await generateRoleplayOpener({
|
||||||
|
selectedModel,
|
||||||
|
assistantPersona: activePersona,
|
||||||
|
userPersona: activeUserPersona,
|
||||||
|
scenarioPrompt,
|
||||||
|
sceneSummary
|
||||||
|
});
|
||||||
|
if (!opener || containsUserVoiceInRoleplayOpener(opener, activeUserPersona)) {
|
||||||
|
return {error: "Failed to regenerate message"};
|
||||||
|
}
|
||||||
|
return {fullReply: opener, promptMessageId: null};
|
||||||
|
}
|
||||||
|
|
||||||
|
const conversation = getRecentChatMessagesUpToIdStmt.all(chatId, promptMessage.id).reverse();
|
||||||
|
const messagesPayload = [];
|
||||||
|
if (activePersona) {
|
||||||
|
messagesPayload.push({
|
||||||
|
role: "system",
|
||||||
|
content: buildPersonaPrompt(activePersona)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (activeUserPersona) {
|
||||||
|
messagesPayload.push({
|
||||||
|
role: "system",
|
||||||
|
content: buildUserPersonaPrompt(activeUserPersona)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (activePersona && sceneSummary) {
|
||||||
|
messagesPayload.push({
|
||||||
|
role: "system",
|
||||||
|
content: buildRoleplayDirectionPrompt({
|
||||||
|
assistantPersona: activePersona,
|
||||||
|
userPersona: activeUserPersona,
|
||||||
|
scenarioPrompt,
|
||||||
|
sceneSummary
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
messagesPayload.push(
|
||||||
|
...conversation.map((m) => ({
|
||||||
|
role: m.role === "bot" ? "assistant" : "user",
|
||||||
|
content: m.content
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
const fullReply = await generateModelReply(selectedModel, messagesPayload);
|
||||||
|
if (!fullReply) {
|
||||||
|
return {error: "Failed to regenerate message"};
|
||||||
|
}
|
||||||
|
return {fullReply, promptMessageId: promptMessage.id};
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseRetryVariants = (rawValue, content) => {
|
||||||
|
if (!rawValue) return [content || ""];
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(rawValue);
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
const normalized = parsed
|
||||||
|
.map(item => String(item || "").trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
if (normalized.length) return normalized;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore invalid stored retry history and fall back to current content.
|
||||||
|
}
|
||||||
|
return [content || ""];
|
||||||
|
};
|
||||||
|
|
||||||
|
const clampRetryActiveIndex = (index, variants) => {
|
||||||
|
const numeric = Number(index);
|
||||||
|
if (!Number.isInteger(numeric) || numeric < 0) return 0;
|
||||||
|
return Math.min(numeric, Math.max(0, variants.length - 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatChatMessage = (row) => {
|
||||||
|
if (!row) return null;
|
||||||
|
const retryVariants = parseRetryVariants(row.retry_variants, row.content);
|
||||||
|
const retryActiveIndex = clampRetryActiveIndex(row.retry_active_index, retryVariants);
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
role: row.role,
|
||||||
|
content: row.content,
|
||||||
|
retryVariants,
|
||||||
|
retryActiveIndex,
|
||||||
|
retryRetriesUsed: Number(row.retry_retries_used || 0),
|
||||||
|
retryPromptMessageId: row.retry_prompt_message_id ?? null
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const persistChatMessageRetryState = ({chatId, messageId, content, retryVariants, retryActiveIndex, retryRetriesUsed, retryPromptMessageId}) => {
|
||||||
|
return updateChatMessageWithRetryStateStmt.run(
|
||||||
|
content,
|
||||||
|
JSON.stringify(retryVariants || [content || ""]),
|
||||||
|
clampRetryActiveIndex(retryActiveIndex, retryVariants || [content || ""]),
|
||||||
|
Number(retryRetriesUsed || 0),
|
||||||
|
retryPromptMessageId ?? null,
|
||||||
|
messageId,
|
||||||
|
chatId
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const insertChatMessage = (chatId, role, content, retryState = null) => {
|
||||||
|
const normalizedContent = String(content || "");
|
||||||
|
const payload = retryState
|
||||||
|
? {
|
||||||
|
retryVariants: Array.isArray(retryState.retryVariants) && retryState.retryVariants.length ? retryState.retryVariants : [normalizedContent],
|
||||||
|
retryActiveIndex: retryState.retryActiveIndex ?? 0,
|
||||||
|
retryRetriesUsed: retryState.retryRetriesUsed ?? 0,
|
||||||
|
retryPromptMessageId: retryState.retryPromptMessageId ?? null
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
return insertChatMessageStmt.run(
|
||||||
|
chatId,
|
||||||
|
role,
|
||||||
|
normalizedContent,
|
||||||
|
payload ? JSON.stringify(payload.retryVariants) : null,
|
||||||
|
payload ? clampRetryActiveIndex(payload.retryActiveIndex, payload.retryVariants) : 0,
|
||||||
|
payload ? Number(payload.retryRetriesUsed || 0) : 0,
|
||||||
|
payload ? payload.retryPromptMessageId : null
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const getChatMessageByIndex = (chatId, index) => {
|
const getChatMessageByIndex = (chatId, index) => {
|
||||||
const safeIndex = Number(index);
|
const safeIndex = Number(index);
|
||||||
if (!Number.isInteger(safeIndex) || safeIndex < 0) return null;
|
if (!Number.isInteger(safeIndex) || safeIndex < 0) return null;
|
||||||
@@ -1385,7 +1559,7 @@ app.get("/chats/:id/messages", requireLogin, (req, res) => {
|
|||||||
if (!session) {
|
if (!session) {
|
||||||
return res.status(404).json({error: "Chat not found"});
|
return res.status(404).json({error: "Chat not found"});
|
||||||
}
|
}
|
||||||
const messages = getChatMessagesStmt.all(chatId);
|
const messages = getChatMessagesStmt.all(chatId).map(formatChatMessage);
|
||||||
res.json({messages, chat: session});
|
res.json({messages, chat: session});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1401,12 +1575,31 @@ app.put("/chats/:id/messages/:messageId", requireLogin, (req, res) => {
|
|||||||
if (!content) {
|
if (!content) {
|
||||||
return res.status(400).json({error: "Message content is required"});
|
return res.status(400).json({error: "Message content is required"});
|
||||||
}
|
}
|
||||||
const result = updateChatMessageStmt.run(content, messageId, chatId);
|
const existingMessage = formatChatMessage(getChatMessageByIdStmt.get(messageId, chatId));
|
||||||
if (result.changes === 0) {
|
if (!existingMessage) {
|
||||||
return res.status(404).json({error: "Message not found"});
|
return res.status(404).json({error: "Message not found"});
|
||||||
}
|
}
|
||||||
|
const requestedRetryActiveIndex = Number(req.body?.retryActiveIndex);
|
||||||
|
if (existingMessage.role === "bot") {
|
||||||
|
const retryVariants = [...existingMessage.retryVariants];
|
||||||
|
const nextActiveIndex = Number.isInteger(requestedRetryActiveIndex)
|
||||||
|
? clampRetryActiveIndex(requestedRetryActiveIndex, retryVariants)
|
||||||
|
: existingMessage.retryActiveIndex;
|
||||||
|
retryVariants[nextActiveIndex] = content;
|
||||||
|
persistChatMessageRetryState({
|
||||||
|
chatId,
|
||||||
|
messageId,
|
||||||
|
content,
|
||||||
|
retryVariants,
|
||||||
|
retryActiveIndex: nextActiveIndex,
|
||||||
|
retryRetriesUsed: existingMessage.retryRetriesUsed,
|
||||||
|
retryPromptMessageId: existingMessage.retryPromptMessageId
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
updateChatMessageStmt.run(content, messageId, chatId);
|
||||||
|
}
|
||||||
touchChatSessionStmt.run(chatId, user);
|
touchChatSessionStmt.run(chatId, user);
|
||||||
const message = getChatMessageByIdStmt.get(messageId, chatId);
|
const message = formatChatMessage(getChatMessageByIdStmt.get(messageId, chatId));
|
||||||
res.json({message});
|
res.json({message});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1425,9 +1618,31 @@ app.put("/chats/:id/messages/by-index/:index", requireLogin, (req, res) => {
|
|||||||
if (!target) {
|
if (!target) {
|
||||||
return res.status(404).json({error: "Message not found"});
|
return res.status(404).json({error: "Message not found"});
|
||||||
}
|
}
|
||||||
updateChatMessageStmt.run(content, target.id, chatId);
|
const existingMessage = formatChatMessage(getChatMessageByIdStmt.get(target.id, chatId));
|
||||||
|
if (!existingMessage) {
|
||||||
|
return res.status(404).json({error: "Message not found"});
|
||||||
|
}
|
||||||
|
const requestedRetryActiveIndex = Number(req.body?.retryActiveIndex);
|
||||||
|
if (existingMessage.role === "bot") {
|
||||||
|
const retryVariants = [...existingMessage.retryVariants];
|
||||||
|
const nextActiveIndex = Number.isInteger(requestedRetryActiveIndex)
|
||||||
|
? clampRetryActiveIndex(requestedRetryActiveIndex, retryVariants)
|
||||||
|
: existingMessage.retryActiveIndex;
|
||||||
|
retryVariants[nextActiveIndex] = content;
|
||||||
|
persistChatMessageRetryState({
|
||||||
|
chatId,
|
||||||
|
messageId: target.id,
|
||||||
|
content,
|
||||||
|
retryVariants,
|
||||||
|
retryActiveIndex: nextActiveIndex,
|
||||||
|
retryRetriesUsed: existingMessage.retryRetriesUsed,
|
||||||
|
retryPromptMessageId: existingMessage.retryPromptMessageId
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
updateChatMessageStmt.run(content, target.id, chatId);
|
||||||
|
}
|
||||||
touchChatSessionStmt.run(chatId, user);
|
touchChatSessionStmt.run(chatId, user);
|
||||||
const message = getChatMessageByIdStmt.get(target.id, chatId);
|
const message = formatChatMessage(getChatMessageByIdStmt.get(target.id, chatId));
|
||||||
res.json({message});
|
res.json({message});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1769,7 +1984,12 @@ app.post("/personas/market/:id/chat", requireLogin, async (req, res) => {
|
|||||||
if (!opener || containsUserVoiceInRoleplayOpener(opener, userPersona)) {
|
if (!opener || containsUserVoiceInRoleplayOpener(opener, userPersona)) {
|
||||||
return res.json({persona, chat, existing: false, generatedInitialMessage: false});
|
return res.json({persona, chat, existing: false, generatedInitialMessage: false});
|
||||||
}
|
}
|
||||||
insertChatMessageStmt.run(chat.id, "bot", opener);
|
insertChatMessage(chat.id, "bot", opener, {
|
||||||
|
retryVariants: [opener],
|
||||||
|
retryActiveIndex: 0,
|
||||||
|
retryRetriesUsed: 0,
|
||||||
|
retryPromptMessageId: null
|
||||||
|
});
|
||||||
touchChatSessionStmt.run(chat.id, user);
|
touchChatSessionStmt.run(chat.id, user);
|
||||||
return res.json({persona, chat: getChatSessionStmt.get(chat.id, user), existing: false, generatedInitialMessage: true, opener});
|
return res.json({persona, chat: getChatSessionStmt.get(chat.id, user), existing: false, generatedInitialMessage: true, opener});
|
||||||
} catch {
|
} catch {
|
||||||
@@ -1830,7 +2050,12 @@ app.post("/roleplays/start", requireLogin, async (req, res) => {
|
|||||||
if (!opener || containsUserVoiceInRoleplayOpener(opener, userPersona)) {
|
if (!opener || containsUserVoiceInRoleplayOpener(opener, userPersona)) {
|
||||||
return res.status(500).json({error: "Failed to generate the character opener"});
|
return res.status(500).json({error: "Failed to generate the character opener"});
|
||||||
}
|
}
|
||||||
insertChatMessageStmt.run(chat.id, "bot", opener);
|
insertChatMessage(chat.id, "bot", opener, {
|
||||||
|
retryVariants: [opener],
|
||||||
|
retryActiveIndex: 0,
|
||||||
|
retryRetriesUsed: 0,
|
||||||
|
retryPromptMessageId: null
|
||||||
|
});
|
||||||
touchChatSessionStmt.run(chat.id, user);
|
touchChatSessionStmt.run(chat.id, user);
|
||||||
return res.json({chat: getChatSessionStmt.get(chat.id, user), existing: false, generatedInitialMessage: true, opener});
|
return res.json({chat: getChatSessionStmt.get(chat.id, user), existing: false, generatedInitialMessage: true, opener});
|
||||||
} catch {
|
} catch {
|
||||||
@@ -1861,7 +2086,7 @@ app.post("/chat", requireLogin, async (req, res) => {
|
|||||||
const history = getRecentChatMessagesStmt.all(targetChatId);
|
const history = getRecentChatMessagesStmt.all(targetChatId);
|
||||||
const conversation = [...history].reverse();
|
const conversation = [...history].reverse();
|
||||||
conversation.push({role: "user", content: message});
|
conversation.push({role: "user", content: message});
|
||||||
insertChatMessageStmt.run(targetChatId, "user", message);
|
insertChatMessage(targetChatId, "user", message);
|
||||||
touchChatSessionStmt.run(targetChatId, user);
|
touchChatSessionStmt.run(targetChatId, user);
|
||||||
const messagesPayload = [];
|
const messagesPayload = [];
|
||||||
if (activePersona) {
|
if (activePersona) {
|
||||||
@@ -1894,7 +2119,12 @@ app.post("/chat", requireLogin, async (req, res) => {
|
|||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
const fullReply = await generateModelReply(selectedModel, messagesPayload);
|
const fullReply = await generateModelReply(selectedModel, messagesPayload);
|
||||||
insertChatMessageStmt.run(targetChatId, "bot", fullReply);
|
insertChatMessage(targetChatId, "bot", fullReply, {
|
||||||
|
retryVariants: [fullReply],
|
||||||
|
retryActiveIndex: 0,
|
||||||
|
retryRetriesUsed: 0,
|
||||||
|
retryPromptMessageId: null
|
||||||
|
});
|
||||||
touchChatSessionStmt.run(targetChatId, user);
|
touchChatSessionStmt.run(targetChatId, user);
|
||||||
console.log("Reply length:", fullReply.length);
|
console.log("Reply length:", fullReply.length);
|
||||||
res.json({reply: fullReply});
|
res.json({reply: fullReply});
|
||||||
@@ -1922,55 +2152,27 @@ app.post("/chats/:id/messages/:messageId/retry", requireLogin, async (req, res)
|
|||||||
if (!latestMessage || latestMessage.id !== messageId || latestMessage.role !== "bot") {
|
if (!latestMessage || latestMessage.id !== messageId || latestMessage.role !== "bot") {
|
||||||
return res.status(400).json({error: "Only the newest assistant message can be retried"});
|
return res.status(400).json({error: "Only the newest assistant message can be retried"});
|
||||||
}
|
}
|
||||||
const promptMessage = getPreviousUserMessageStmt.get(chatId, messageId);
|
|
||||||
if (!promptMessage) {
|
|
||||||
return res.status(400).json({error: "No previous user prompt found for retry"});
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const activePersona = getAssistantPersonaForChatStmt.get(chatId, user);
|
const retryResult = await buildRetryPayload({chatId, user, session, selectedModel, targetMessage});
|
||||||
const activeUserPersona = getUserPersonaForChatStmt.get(chatId, user) || getActiveUserPersonaStmt.get(user);
|
if (retryResult.error) {
|
||||||
const sceneSummary = session.scenario_summary;
|
return res.status(400).json({error: retryResult.error});
|
||||||
const scenarioPrompt = session.scenario_prompt;
|
|
||||||
const conversation = getRecentChatMessagesUpToIdStmt.all(chatId, promptMessage.id).reverse();
|
|
||||||
const messagesPayload = [];
|
|
||||||
if (activePersona) {
|
|
||||||
messagesPayload.push({
|
|
||||||
role: "system",
|
|
||||||
content: buildPersonaPrompt(activePersona)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (activeUserPersona) {
|
const {fullReply, promptMessageId} = retryResult;
|
||||||
messagesPayload.push({
|
const existingMessage = formatChatMessage(targetMessage);
|
||||||
role: "system",
|
const retryVariants = [...existingMessage.retryVariants, fullReply];
|
||||||
content: buildUserPersonaPrompt(activeUserPersona)
|
const retryActiveIndex = retryVariants.length - 1;
|
||||||
});
|
const retryRetriesUsed = existingMessage.retryRetriesUsed + 1;
|
||||||
}
|
persistChatMessageRetryState({
|
||||||
if (activePersona && sceneSummary) {
|
chatId,
|
||||||
messagesPayload.push({
|
messageId,
|
||||||
role: "system",
|
content: fullReply,
|
||||||
content: buildRoleplayDirectionPrompt({
|
retryVariants,
|
||||||
assistantPersona: activePersona,
|
retryActiveIndex,
|
||||||
userPersona: activeUserPersona,
|
retryRetriesUsed,
|
||||||
scenarioPrompt,
|
retryPromptMessageId: promptMessageId
|
||||||
sceneSummary
|
});
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
messagesPayload.push(
|
|
||||||
...conversation.map((m) => ({
|
|
||||||
role: m.role === "bot" ? "assistant" : "user",
|
|
||||||
content: m.content
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
const fullReply = await generateModelReply(selectedModel, messagesPayload);
|
|
||||||
if (!fullReply) {
|
|
||||||
return res.status(500).json({error: "Failed to regenerate message"});
|
|
||||||
}
|
|
||||||
updateChatMessageStmt.run(fullReply, messageId, chatId);
|
|
||||||
touchChatSessionStmt.run(chatId, user);
|
touchChatSessionStmt.run(chatId, user);
|
||||||
return res.json({message: {id: messageId, role: "bot", content: fullReply}, promptMessageId: promptMessage.id});
|
return res.json({message: formatChatMessage(getChatMessageByIdStmt.get(messageId, chatId)), promptMessageId});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Retry error:", err);
|
console.error("Retry error:", err);
|
||||||
return res.status(500).json({error: "Failed to regenerate message"});
|
return res.status(500).json({error: "Failed to regenerate message"});
|
||||||
@@ -1994,55 +2196,27 @@ app.post("/chats/:id/messages/by-index/:index/retry", requireLogin, async (req,
|
|||||||
if (!latestMessage || latestMessage.id !== targetMessage.id || latestMessage.role !== "bot") {
|
if (!latestMessage || latestMessage.id !== targetMessage.id || latestMessage.role !== "bot") {
|
||||||
return res.status(400).json({error: "Only the newest assistant message can be retried"});
|
return res.status(400).json({error: "Only the newest assistant message can be retried"});
|
||||||
}
|
}
|
||||||
const promptMessage = getPreviousUserMessageStmt.get(chatId, targetMessage.id);
|
|
||||||
if (!promptMessage) {
|
|
||||||
return res.status(400).json({error: "No previous user prompt found for retry"});
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const activePersona = getAssistantPersonaForChatStmt.get(chatId, user);
|
const retryResult = await buildRetryPayload({chatId, user, session, selectedModel, targetMessage});
|
||||||
const activeUserPersona = getUserPersonaForChatStmt.get(chatId, user) || getActiveUserPersonaStmt.get(user);
|
if (retryResult.error) {
|
||||||
const sceneSummary = session.scenario_summary;
|
return res.status(400).json({error: retryResult.error});
|
||||||
const scenarioPrompt = session.scenario_prompt;
|
|
||||||
const conversation = getRecentChatMessagesUpToIdStmt.all(chatId, promptMessage.id).reverse();
|
|
||||||
const messagesPayload = [];
|
|
||||||
if (activePersona) {
|
|
||||||
messagesPayload.push({
|
|
||||||
role: "system",
|
|
||||||
content: buildPersonaPrompt(activePersona)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (activeUserPersona) {
|
const {fullReply, promptMessageId} = retryResult;
|
||||||
messagesPayload.push({
|
const existingMessage = formatChatMessage(targetMessage);
|
||||||
role: "system",
|
const retryVariants = [...existingMessage.retryVariants, fullReply];
|
||||||
content: buildUserPersonaPrompt(activeUserPersona)
|
const retryActiveIndex = retryVariants.length - 1;
|
||||||
});
|
const retryRetriesUsed = existingMessage.retryRetriesUsed + 1;
|
||||||
}
|
persistChatMessageRetryState({
|
||||||
if (activePersona && sceneSummary) {
|
chatId,
|
||||||
messagesPayload.push({
|
messageId: targetMessage.id,
|
||||||
role: "system",
|
content: fullReply,
|
||||||
content: buildRoleplayDirectionPrompt({
|
retryVariants,
|
||||||
assistantPersona: activePersona,
|
retryActiveIndex,
|
||||||
userPersona: activeUserPersona,
|
retryRetriesUsed,
|
||||||
scenarioPrompt,
|
retryPromptMessageId: promptMessageId
|
||||||
sceneSummary
|
});
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
messagesPayload.push(
|
|
||||||
...conversation.map((m) => ({
|
|
||||||
role: m.role === "bot" ? "assistant" : "user",
|
|
||||||
content: m.content
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
const fullReply = await generateModelReply(selectedModel, messagesPayload);
|
|
||||||
if (!fullReply) {
|
|
||||||
return res.status(500).json({error: "Failed to regenerate message"});
|
|
||||||
}
|
|
||||||
updateChatMessageStmt.run(fullReply, targetMessage.id, chatId);
|
|
||||||
touchChatSessionStmt.run(chatId, user);
|
touchChatSessionStmt.run(chatId, user);
|
||||||
return res.json({message: {id: targetMessage.id, role: "bot", content: fullReply}, promptMessageId: promptMessage.id});
|
return res.json({message: formatChatMessage(getChatMessageByIdStmt.get(targetMessage.id, chatId)), promptMessageId});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Retry error:", err);
|
console.error("Retry error:", err);
|
||||||
return res.status(500).json({error: "Failed to regenerate message"});
|
return res.status(500).json({error: "Failed to regenerate message"});
|
||||||
|
|||||||
Reference in New Issue
Block a user