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> </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">
+13
View File
@@ -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>
+34
View File
@@ -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
View File
@@ -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});
+63
View File
@@ -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);
} }
+283 -109
View File
@@ -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"});