add more features
This commit is contained in:
@@ -216,7 +216,6 @@
|
|||||||
<div class="input-shell input-shell-focus">
|
<div class="input-shell input-shell-focus">
|
||||||
<div class="input-meta">
|
<div class="input-meta">
|
||||||
<p class="composer-hint">Enter to send · Shift+Enter for new line</p>
|
<p class="composer-hint">Enter to send · Shift+Enter for new line</p>
|
||||||
<p id="appNotice" class="status inline-status hidden" aria-live="polite"></p>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="quickPrompts" class="quick-prompts" aria-label="Suggested starters"></div>
|
<div id="quickPrompts" class="quick-prompts" aria-label="Suggested starters"></div>
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
|
|||||||
@@ -108,6 +108,10 @@
|
|||||||
<span>You in this roleplay</span>
|
<span>You in this roleplay</span>
|
||||||
<select id="marketPreviewUserPersonaSelect"></select>
|
<select id="marketPreviewUserPersonaSelect"></select>
|
||||||
</label>
|
</label>
|
||||||
|
<label id="marketPreviewScenarioField" class="field market-preview-scenario-field hidden">
|
||||||
|
<span>Scenario (optional)</span>
|
||||||
|
<textarea id="marketPreviewScenario" rows="3" maxlength="240" placeholder="Set the opening scene you want the AI character to start with."></textarea>
|
||||||
|
</label>
|
||||||
<button id="marketPreviewConfirm" type="button" class="primary-action">Take persona</button>
|
<button id="marketPreviewConfirm" type="button" class="primary-action">Take persona</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+53
-8
@@ -18,6 +18,8 @@ const marketPreviewDetails = $("marketPreviewDetails");
|
|||||||
const marketPreviewExampleDialogues = $("marketPreviewExampleDialogues");
|
const marketPreviewExampleDialogues = $("marketPreviewExampleDialogues");
|
||||||
const marketPreviewUserPersonaField = $("marketPreviewUserPersonaField");
|
const marketPreviewUserPersonaField = $("marketPreviewUserPersonaField");
|
||||||
const marketPreviewUserPersonaSelect = $("marketPreviewUserPersonaSelect");
|
const marketPreviewUserPersonaSelect = $("marketPreviewUserPersonaSelect");
|
||||||
|
const marketPreviewScenarioField = $("marketPreviewScenarioField");
|
||||||
|
const marketPreviewScenario = $("marketPreviewScenario");
|
||||||
const marketPreviewConfirm = $("marketPreviewConfirm");
|
const marketPreviewConfirm = $("marketPreviewConfirm");
|
||||||
const marketPreviewClose = $("marketPreviewClose");
|
const marketPreviewClose = $("marketPreviewClose");
|
||||||
const marketPersonaModal = $("marketPersonaModal");
|
const marketPersonaModal = $("marketPersonaModal");
|
||||||
@@ -112,10 +114,45 @@ function formatPersonaField(value) {
|
|||||||
return value && value.trim() ? value.trim() : "Not provided.";
|
return value && value.trim() ? value.trim() : "Not provided.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setCollapsiblePreviewText(element, value, maxLength = 220) {
|
||||||
|
const text = formatPersonaField(value);
|
||||||
|
element.dataset.fullText = text;
|
||||||
|
let toggle = element.nextElementSibling;
|
||||||
|
if (!toggle || !toggle.classList.contains("market-preview-toggle")) {
|
||||||
|
toggle = document.createElement("button");
|
||||||
|
toggle.type = "button";
|
||||||
|
toggle.className = "market-preview-toggle hidden";
|
||||||
|
element.insertAdjacentElement("afterend", toggle);
|
||||||
|
toggle.addEventListener("click", () => {
|
||||||
|
const isExpanded = element.dataset.expanded === "true";
|
||||||
|
element.dataset.expanded = isExpanded ? "false" : "true";
|
||||||
|
if (element.dataset.expanded === "true") {
|
||||||
|
element.textContent = element.dataset.fullText;
|
||||||
|
toggle.textContent = "Show less";
|
||||||
|
} else {
|
||||||
|
element.textContent = `${element.dataset.fullText.slice(0, maxLength).trimEnd()}...`;
|
||||||
|
toggle.textContent = "Show more";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldCollapse = text !== "Not provided." && text.length > maxLength;
|
||||||
|
element.dataset.expanded = "false";
|
||||||
|
if (shouldCollapse) {
|
||||||
|
element.textContent = `${text.slice(0, maxLength).trimEnd()}...`;
|
||||||
|
toggle.textContent = "Show more";
|
||||||
|
toggle.classList.remove("hidden");
|
||||||
|
} else {
|
||||||
|
element.textContent = text;
|
||||||
|
toggle.classList.add("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function closeMarketPreview() {
|
function closeMarketPreview() {
|
||||||
marketPreviewModal.classList.add("hidden");
|
marketPreviewModal.classList.add("hidden");
|
||||||
pendingMarketPersona = null;
|
pendingMarketPersona = null;
|
||||||
marketPreviewUserPersonaSelect.innerHTML = "";
|
marketPreviewUserPersonaSelect.innerHTML = "";
|
||||||
|
marketPreviewScenario.value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function populateUserPersonaChoices() {
|
function populateUserPersonaChoices() {
|
||||||
@@ -151,14 +188,15 @@ function openMarketPreview(persona) {
|
|||||||
pendingMarketPersona = persona;
|
pendingMarketPersona = persona;
|
||||||
marketPreviewName.textContent = persona.name;
|
marketPreviewName.textContent = persona.name;
|
||||||
marketPreviewMeta.textContent = buildPersonaMeta(persona);
|
marketPreviewMeta.textContent = buildPersonaMeta(persona);
|
||||||
marketPreviewPronouns.textContent = formatPersonaField(persona.pronouns);
|
setCollapsiblePreviewText(marketPreviewPronouns, persona.pronouns, 80);
|
||||||
marketPreviewAppearance.textContent = formatPersonaField(persona.appearance);
|
setCollapsiblePreviewText(marketPreviewAppearance, persona.appearance);
|
||||||
marketPreviewBackground.textContent = formatPersonaField(persona.background);
|
setCollapsiblePreviewText(marketPreviewBackground, persona.background);
|
||||||
marketPreviewDetails.textContent = formatPersonaField(persona.details);
|
setCollapsiblePreviewText(marketPreviewDetails, persona.details);
|
||||||
marketPreviewExampleDialogues.textContent = formatPersonaField(persona.example_dialogues);
|
setCollapsiblePreviewText(marketPreviewExampleDialogues, persona.example_dialogues);
|
||||||
|
|
||||||
const isAssistant = persona.persona_type === "assistant";
|
const isAssistant = persona.persona_type === "assistant";
|
||||||
marketPreviewUserPersonaField.classList.toggle("hidden", !isAssistant);
|
marketPreviewUserPersonaField.classList.toggle("hidden", !isAssistant);
|
||||||
|
marketPreviewScenarioField.classList.toggle("hidden", !isAssistant);
|
||||||
marketPreviewConfirm.textContent = isAssistant ? "Start roleplay" : "Collect persona";
|
marketPreviewConfirm.textContent = isAssistant ? "Start roleplay" : "Collect persona";
|
||||||
if (isAssistant) {
|
if (isAssistant) {
|
||||||
populateUserPersonaChoices();
|
populateUserPersonaChoices();
|
||||||
@@ -303,7 +341,7 @@ async function collectMarketPersona(marketId) {
|
|||||||
setMarketStatus("User Persona collected.", "success");
|
setMarketStatus("User Persona collected.", "success");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startMarketPersonaChat(marketId, userPersonaSelection) {
|
async function startMarketPersonaChat(marketId, userPersonaSelection, scenarioPrompt = "") {
|
||||||
if (!userPersonaSelection) {
|
if (!userPersonaSelection) {
|
||||||
setMarketStatus("Choose who you are before starting roleplay.", "error");
|
setMarketStatus("Choose who you are before starting roleplay.", "error");
|
||||||
return;
|
return;
|
||||||
@@ -315,7 +353,10 @@ async function startMarketPersonaChat(marketId, userPersonaSelection) {
|
|||||||
|
|
||||||
setMarketStatus("Starting AI Character chat...");
|
setMarketStatus("Starting AI Character chat...");
|
||||||
const userPersonaId = userPersonaSelection === "self" ? null : Number(userPersonaSelection);
|
const userPersonaId = userPersonaSelection === "self" ? null : Number(userPersonaSelection);
|
||||||
const res = await post(`/personas/market/${marketId}/chat`, {userPersonaId});
|
const res = await post(`/personas/market/${marketId}/chat`, {
|
||||||
|
userPersonaId,
|
||||||
|
scenarioPrompt: scenarioPrompt.trim()
|
||||||
|
});
|
||||||
if (res.error || !res.chat) {
|
if (res.error || !res.chat) {
|
||||||
setMarketStatus(res.error || "Unable to start chat.", "error");
|
setMarketStatus(res.error || "Unable to start chat.", "error");
|
||||||
return;
|
return;
|
||||||
@@ -385,7 +426,11 @@ marketPreviewConfirm.addEventListener("click", async () => {
|
|||||||
if (!pendingMarketPersona) return;
|
if (!pendingMarketPersona) return;
|
||||||
const marketId = pendingMarketPersona.id;
|
const marketId = pendingMarketPersona.id;
|
||||||
if (pendingMarketPersona.persona_type === "assistant") {
|
if (pendingMarketPersona.persona_type === "assistant") {
|
||||||
await startMarketPersonaChat(marketId, marketPreviewUserPersonaSelect.value || "self");
|
await startMarketPersonaChat(
|
||||||
|
marketId,
|
||||||
|
marketPreviewUserPersonaSelect.value || "self",
|
||||||
|
marketPreviewScenario.value || ""
|
||||||
|
);
|
||||||
closeMarketPreview();
|
closeMarketPreview();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
+322
-23
@@ -10,6 +10,7 @@ const registerUsernameInput = $("registerUsername"), registerPasswordInput = $("
|
|||||||
const loginSubmit = $("loginSubmit"), registerSubmit = $("registerSubmit");
|
const loginSubmit = $("loginSubmit"), registerSubmit = $("registerSubmit");
|
||||||
const chatList = $("chatList"), chatSearchInput = $("chatSearch"), newChatBtn = $("newChat");
|
const chatList = $("chatList"), chatSearchInput = $("chatSearch"), newChatBtn = $("newChat");
|
||||||
const startRoleplayBtn = $("startRoleplay");
|
const startRoleplayBtn = $("startRoleplay");
|
||||||
|
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 contextChipButton = $("contextChipButton"), contextPopover = $("contextPopover"), contextSummaryLabel = $("contextSummaryLabel");
|
const contextChipButton = $("contextChipButton"), contextPopover = $("contextPopover"), contextSummaryLabel = $("contextSummaryLabel");
|
||||||
@@ -46,6 +47,8 @@ let roleplayPresetCharacterId = null;
|
|||||||
let noticeTimer = null;
|
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 messageRetryState = new Map();
|
||||||
|
|
||||||
const onboardingPrompts = {
|
const onboardingPrompts = {
|
||||||
ask: ["Explain this topic in simple terms.", "Compare these options and recommend one."],
|
ask: ["Explain this topic in simple terms.", "Compare these options and recommend one."],
|
||||||
@@ -113,6 +116,21 @@ function setPersonaFormNotice(message = "", state = "") {
|
|||||||
personaFormNotice.className = state ? `status ${state}` : "status";
|
personaFormNotice.className = state ? `status ${state}` : "status";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setChatLoading(active, message = "Loading chats...") {
|
||||||
|
if (!chatListLoading || !chatListLoadingText) return;
|
||||||
|
if (active) {
|
||||||
|
chatLoadingDepth += 1;
|
||||||
|
chatListLoadingText.textContent = message;
|
||||||
|
chatListLoading.classList.remove("hidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chatLoadingDepth = Math.max(0, chatLoadingDepth - 1);
|
||||||
|
if (chatLoadingDepth === 0) {
|
||||||
|
chatListLoading.classList.add("hidden");
|
||||||
|
chatListLoadingText.textContent = "Loading chats...";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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;
|
||||||
@@ -245,16 +263,6 @@ function updateRoleplaySuggestions() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
function getQuickPromptsForChat(chat) {
|
function getQuickPromptsForChat(chat) {
|
||||||
if (chat?.assistant_persona_id) {
|
|
||||||
const characterName = chat.assistant_persona_name || chat.title || "the character";
|
|
||||||
const userName = chat.user_persona_name || "me";
|
|
||||||
return [
|
|
||||||
`Stay in character and describe what ${characterName} notices about ${userName} right now.`,
|
|
||||||
`Push the scene forward with a new complication.`,
|
|
||||||
`Respond with sharper tension and more specific detail.`,
|
|
||||||
`Let ${characterName} reveal what they actually want from me.`
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
function renderQuickPrompts() {
|
function renderQuickPrompts() {
|
||||||
@@ -295,7 +303,195 @@ function updateSendState() {
|
|||||||
sendBtn.disabled = isProcessing || !hasText;
|
sendBtn.disabled = isProcessing || !hasText;
|
||||||
}
|
}
|
||||||
|
|
||||||
function addMessage(content, isUser = false, isLoading = false) {
|
function getNewestMessage() {
|
||||||
|
return currentMessages.length ? currentMessages[currentMessages.length - 1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMessageIndexById(messageId) {
|
||||||
|
return currentMessages.findIndex((message) => message.id === messageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveMessageTarget(messageId, fallbackIndex = -1) {
|
||||||
|
if (messageId) {
|
||||||
|
const index = getMessageIndexById(messageId);
|
||||||
|
if (index >= 0) {
|
||||||
|
return {index, message: currentMessages[index]};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (activeChatId) {
|
||||||
|
await setActiveChat(activeChatId);
|
||||||
|
}
|
||||||
|
if (messageId) {
|
||||||
|
const index = getMessageIndexById(messageId);
|
||||||
|
if (index >= 0) {
|
||||||
|
return {index, message: currentMessages[index]};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fallbackIndex >= 0 && fallbackIndex < currentMessages.length) {
|
||||||
|
return {index: fallbackIndex, message: currentMessages[fallbackIndex]};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMessageEndpoint(target, suffix = "") {
|
||||||
|
if (!activeChatId || !target) return "";
|
||||||
|
if (target.message?.id) return `/chats/${activeChatId}/messages/${target.message.id}${suffix}`;
|
||||||
|
if (target.index >= 0) return `/chats/${activeChatId}/messages/by-index/${target.index}${suffix}`;
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPreviousUserMessageId(messageId) {
|
||||||
|
const index = getMessageIndexById(messageId);
|
||||||
|
if (index <= 0) return null;
|
||||||
|
for (let i = index - 1; i >= 0; i -= 1) {
|
||||||
|
if (currentMessages[i].role === "user" && currentMessages[i].id) {
|
||||||
|
return currentMessages[i].id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureRetryState(message) {
|
||||||
|
if (!message?.id) return null;
|
||||||
|
let state = messageRetryState.get(message.id);
|
||||||
|
if (!state) {
|
||||||
|
state = {
|
||||||
|
variants: [message.content || ""],
|
||||||
|
activeIndex: 0,
|
||||||
|
retriesUsed: 0,
|
||||||
|
promptMessageId: getPreviousUserMessageId(message.id)
|
||||||
|
};
|
||||||
|
messageRetryState.set(message.id, state);
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function editChatMessage(messageId, fallbackIndex = -1) {
|
||||||
|
const target = await resolveMessageTarget(messageId, fallbackIndex);
|
||||||
|
if (!target) return setNotice("Message is not ready yet. Try again.", "error");
|
||||||
|
const {index, message} = target;
|
||||||
|
const nextContent = await promptPopup({
|
||||||
|
eyebrow: message.role === "user" ? "Your message" : "Assistant message",
|
||||||
|
title: "Edit message",
|
||||||
|
description: "Update this message.",
|
||||||
|
label: "Message",
|
||||||
|
value: message.content,
|
||||||
|
confirmLabel: "Save"
|
||||||
|
});
|
||||||
|
const content = (nextContent || "").trim();
|
||||||
|
if (!content) return;
|
||||||
|
const endpoint = getMessageEndpoint(target);
|
||||||
|
if (!endpoint) return setNotice("Message is not ready yet. Try again.", "error");
|
||||||
|
const res = await put(endpoint, {content});
|
||||||
|
if (res.error || !res.message) {
|
||||||
|
return setNotice(res.error || "Unable to edit message.", "error");
|
||||||
|
}
|
||||||
|
currentMessages[index].id = res.message.id || currentMessages[index].id;
|
||||||
|
currentMessages[index].content = res.message.content;
|
||||||
|
const retryState = message.id ? messageRetryState.get(message.id) : null;
|
||||||
|
if (retryState) {
|
||||||
|
retryState.variants[retryState.activeIndex] = res.message.content;
|
||||||
|
}
|
||||||
|
renderMessages();
|
||||||
|
setNotice("Message updated.", "success");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteChatMessage(messageId, fallbackIndex = -1) {
|
||||||
|
const target = await resolveMessageTarget(messageId, fallbackIndex);
|
||||||
|
if (!target) return setNotice("Message is not ready yet. Try again.", "error");
|
||||||
|
const {index, message} = target;
|
||||||
|
const confirmed = await confirmPopup({
|
||||||
|
eyebrow: message.role === "user" ? "Your message" : "Assistant message",
|
||||||
|
title: "Delete message",
|
||||||
|
description: "Delete this message from the chat?",
|
||||||
|
confirmLabel: "Delete",
|
||||||
|
danger: true
|
||||||
|
});
|
||||||
|
if (!confirmed) return;
|
||||||
|
const endpoint = getMessageEndpoint(target);
|
||||||
|
if (!endpoint) return setNotice("Message is not ready yet. Try again.", "error");
|
||||||
|
const res = await del(endpoint);
|
||||||
|
if (res.error) return setNotice(res.error, "error");
|
||||||
|
currentMessages = currentMessages.filter((item) => item.id !== message.id);
|
||||||
|
if (message.id) {
|
||||||
|
messageRetryState.delete(message.id);
|
||||||
|
for (const [key, state] of messageRetryState.entries()) {
|
||||||
|
if (state.promptMessageId === message.id) {
|
||||||
|
messageRetryState.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
renderMessages();
|
||||||
|
await loadSummary();
|
||||||
|
setNotice("Message deleted.", "success");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function retryLatestAssistantMessage(messageId, fallbackIndex = -1) {
|
||||||
|
const target = await resolveMessageTarget(messageId, fallbackIndex);
|
||||||
|
if (!target) return setNotice("Message is not ready yet. Try again.", "error");
|
||||||
|
const newest = getNewestMessage();
|
||||||
|
const sameNewest = target.index === currentMessages.length - 1;
|
||||||
|
if (!newest || !sameNewest || newest.role !== "bot") {
|
||||||
|
return setNotice("Only the newest assistant message can be retried.", "error");
|
||||||
|
}
|
||||||
|
const state = ensureRetryState(newest) || {variants: [newest.content || ""], activeIndex: 0, retriesUsed: 0, promptMessageId: null};
|
||||||
|
if (!state || state.retriesUsed >= 5) {
|
||||||
|
return setNotice("Retry limit reached (5).", "error");
|
||||||
|
}
|
||||||
|
setLoadingState(true);
|
||||||
|
setNotice("Regenerating reply...");
|
||||||
|
try {
|
||||||
|
const endpoint = getMessageEndpoint(target, "/retry");
|
||||||
|
if (!endpoint) return setNotice("Message is not ready yet. Try again.", "error");
|
||||||
|
const res = await post(endpoint, {model: modelSelect.value});
|
||||||
|
if (res.error || !res.message?.content) {
|
||||||
|
return setNotice(res.error || "Unable to retry message.", "error");
|
||||||
|
}
|
||||||
|
state.retriesUsed += 1;
|
||||||
|
state.promptMessageId = res.promptMessageId || state.promptMessageId;
|
||||||
|
state.variants.push(res.message.content);
|
||||||
|
state.activeIndex = state.variants.length - 1;
|
||||||
|
const updateIndex = target.message?.id ? getMessageIndexById(target.message.id) : target.index;
|
||||||
|
if (updateIndex >= 0) {
|
||||||
|
currentMessages[updateIndex].id = res.message.id || currentMessages[updateIndex].id;
|
||||||
|
currentMessages[updateIndex].content = res.message.content;
|
||||||
|
}
|
||||||
|
renderMessages();
|
||||||
|
setNotice(`Reply regenerated (${state.retriesUsed}/5).`, "success");
|
||||||
|
} finally {
|
||||||
|
setLoadingState(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function switchRetryVariant(messageId, direction, fallbackIndex = -1) {
|
||||||
|
const target = await resolveMessageTarget(messageId, fallbackIndex);
|
||||||
|
if (!target) return setNotice("Message is not ready yet. Try again.", "error");
|
||||||
|
const newest = getNewestMessage();
|
||||||
|
const sameNewest = target.index === currentMessages.length - 1;
|
||||||
|
if (!newest || !sameNewest || newest.role !== "bot") {
|
||||||
|
return setNotice("Variants can only be switched on the newest assistant message.", "error");
|
||||||
|
}
|
||||||
|
const state = target.message?.id ? messageRetryState.get(target.message.id) : null;
|
||||||
|
if (!state || state.variants.length < 2) return;
|
||||||
|
const nextIndex = state.activeIndex + direction;
|
||||||
|
if (nextIndex < 0 || nextIndex >= state.variants.length) return;
|
||||||
|
const targetContent = state.variants[nextIndex];
|
||||||
|
const endpoint = getMessageEndpoint(target);
|
||||||
|
if (!endpoint) return setNotice("Message is not ready yet. Try again.", "error");
|
||||||
|
const res = await put(endpoint, {content: targetContent});
|
||||||
|
if (res.error || !res.message) {
|
||||||
|
return setNotice(res.error || "Unable to switch variant.", "error");
|
||||||
|
}
|
||||||
|
state.activeIndex = nextIndex;
|
||||||
|
const updateIndex = target.message?.id ? getMessageIndexById(target.message.id) : target.index;
|
||||||
|
if (updateIndex >= 0) {
|
||||||
|
currentMessages[updateIndex].id = res.message.id || currentMessages[updateIndex].id;
|
||||||
|
currentMessages[updateIndex].content = res.message.content;
|
||||||
|
}
|
||||||
|
renderMessages();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMessage(contentOrMessage, isUser = false, isLoading = false, options = {}) {
|
||||||
const msgDiv = document.createElement("div");
|
const msgDiv = document.createElement("div");
|
||||||
msgDiv.className = `msg ${isUser ? "user" : "bot"}${isLoading ? " loading" : ""}`;
|
msgDiv.className = `msg ${isUser ? "user" : "bot"}${isLoading ? " loading" : ""}`;
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
@@ -312,12 +508,78 @@ function addMessage(content, isUser = false, isLoading = false) {
|
|||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
} else {
|
} else {
|
||||||
const header = document.createElement("div"), body = document.createElement("div");
|
const message = typeof contentOrMessage === "object" && contentOrMessage
|
||||||
|
? contentOrMessage
|
||||||
|
: {id: null, role: isUser ? "user" : "bot", content: String(contentOrMessage || "")};
|
||||||
|
const isMessageUser = message.role === "user";
|
||||||
|
msgDiv.className = `msg ${isMessageUser ? "user" : "bot"}`;
|
||||||
|
const header = document.createElement("div"), body = document.createElement("div"), actions = document.createElement("div");
|
||||||
header.className = "msg-header"; body.className = "msg-content";
|
header.className = "msg-header"; body.className = "msg-content";
|
||||||
|
actions.className = "msg-actions";
|
||||||
const activeChat = getChatById(activeChatId);
|
const activeChat = getChatById(activeChatId);
|
||||||
header.textContent = isUser ? "You" : (activeChat?.assistant_persona_name || "Assistant");
|
header.textContent = isMessageUser ? "You" : (activeChat?.assistant_persona_name || "Assistant");
|
||||||
setMessageContent(body, content);
|
setMessageContent(body, message.content);
|
||||||
|
|
||||||
|
const editBtn = document.createElement("button");
|
||||||
|
editBtn.type = "button";
|
||||||
|
editBtn.className = "msg-action-btn";
|
||||||
|
editBtn.textContent = "Edit";
|
||||||
|
editBtn.addEventListener("click", () => {
|
||||||
|
void editChatMessage(message.id, options.messageIndex ?? -1);
|
||||||
|
});
|
||||||
|
actions.appendChild(editBtn);
|
||||||
|
|
||||||
|
const deleteBtn = document.createElement("button");
|
||||||
|
deleteBtn.type = "button";
|
||||||
|
deleteBtn.className = "msg-action-btn";
|
||||||
|
deleteBtn.textContent = "Delete";
|
||||||
|
deleteBtn.addEventListener("click", () => {
|
||||||
|
void deleteChatMessage(message.id, options.messageIndex ?? -1);
|
||||||
|
});
|
||||||
|
actions.appendChild(deleteBtn);
|
||||||
|
|
||||||
|
if (options.isNewest && message.role === "bot") {
|
||||||
|
const state = message.id ? ensureRetryState(message) : {retriesUsed: 0, variants: [], activeIndex: 0};
|
||||||
|
const retryBtn = document.createElement("button");
|
||||||
|
retryBtn.type = "button";
|
||||||
|
retryBtn.className = "msg-action-btn";
|
||||||
|
retryBtn.textContent = `Retry (${state.retriesUsed}/5)`;
|
||||||
|
retryBtn.disabled = state.retriesUsed >= 5 || isProcessing;
|
||||||
|
retryBtn.addEventListener("click", () => {
|
||||||
|
void retryLatestAssistantMessage(message.id, options.messageIndex ?? -1);
|
||||||
|
});
|
||||||
|
actions.appendChild(retryBtn);
|
||||||
|
|
||||||
|
if (state.variants.length > 1) {
|
||||||
|
const prevBtn = document.createElement("button");
|
||||||
|
prevBtn.type = "button";
|
||||||
|
prevBtn.className = "msg-action-btn";
|
||||||
|
prevBtn.textContent = "Prev";
|
||||||
|
prevBtn.disabled = state.activeIndex <= 0;
|
||||||
|
prevBtn.addEventListener("click", () => {
|
||||||
|
void switchRetryVariant(message.id, -1, options.messageIndex ?? -1);
|
||||||
|
});
|
||||||
|
|
||||||
|
const nextBtn = document.createElement("button");
|
||||||
|
nextBtn.type = "button";
|
||||||
|
nextBtn.className = "msg-action-btn";
|
||||||
|
nextBtn.textContent = "Next";
|
||||||
|
nextBtn.disabled = state.activeIndex >= state.variants.length - 1;
|
||||||
|
nextBtn.addEventListener("click", () => {
|
||||||
|
void switchRetryVariant(message.id, 1, options.messageIndex ?? -1);
|
||||||
|
});
|
||||||
|
|
||||||
|
const indexBadge = document.createElement("span");
|
||||||
|
indexBadge.className = "msg-variant-index";
|
||||||
|
indexBadge.textContent = `${state.activeIndex + 1}/${state.variants.length}`;
|
||||||
|
actions.append(prevBtn, indexBadge, nextBtn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
msgDiv.append(header, body);
|
msgDiv.append(header, body);
|
||||||
|
if (actions.childElementCount) {
|
||||||
|
msgDiv.appendChild(actions);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
messagesDiv.appendChild(msgDiv);
|
messagesDiv.appendChild(msgDiv);
|
||||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||||
@@ -330,7 +592,10 @@ function renderMessages() {
|
|||||||
if (!currentMessages.length && (!chat || chat.assistant_persona_id)) {
|
if (!currentMessages.length && (!chat || chat.assistant_persona_id)) {
|
||||||
renderMessageEmptyState();
|
renderMessageEmptyState();
|
||||||
} else {
|
} else {
|
||||||
currentMessages.forEach((msg) => addMessage(msg.content, msg.role === "user"));
|
currentMessages.forEach((msg, index) => addMessage(msg, msg.role === "user", false, {
|
||||||
|
messageIndex: index,
|
||||||
|
isNewest: index === currentMessages.length - 1
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
updateChatParticipants();
|
updateChatParticipants();
|
||||||
updateChatActionState();
|
updateChatActionState();
|
||||||
@@ -889,22 +1154,33 @@ function showAuthScreen(target) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function setActiveChat(id) {
|
async function setActiveChat(id) {
|
||||||
|
setChatLoading(true, "Opening chat...");
|
||||||
activeChatId = id;
|
activeChatId = id;
|
||||||
|
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) return setNotice(res.error, "error");
|
if (res.error) {
|
||||||
|
setChatLoading(false);
|
||||||
|
return setNotice(res.error, "error");
|
||||||
|
}
|
||||||
const existing = getChatById(id);
|
const existing = getChatById(id);
|
||||||
if (res.chat && existing) Object.assign(existing, res.chat);
|
if (res.chat && existing) Object.assign(existing, res.chat);
|
||||||
currentMessages = res.messages || []; renderMessages();
|
currentMessages = res.messages || []; renderMessages();
|
||||||
updateSendState();
|
updateSendState();
|
||||||
|
setChatLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadChatSessions() {
|
async function loadChatSessions() {
|
||||||
|
setChatLoading(true, "Loading chats...");
|
||||||
const res = await get("/chats");
|
const res = await get("/chats");
|
||||||
if (res.error) return setNotice(res.error, "error");
|
if (res.error) {
|
||||||
|
setChatLoading(false);
|
||||||
|
return setNotice(res.error, "error");
|
||||||
|
}
|
||||||
chatSessions = res.chats || []; renderChatList();
|
chatSessions = res.chats || []; renderChatList();
|
||||||
if (chatSessions.length) {
|
if (chatSessions.length) {
|
||||||
const preferredChatId = (requestedChatId && getChatById(requestedChatId)) ? requestedChatId : activeChatId && getChatById(activeChatId) ? activeChatId : chatSessions[0].id;
|
const preferredChatId = (requestedChatId && getChatById(requestedChatId)) ? requestedChatId : activeChatId && getChatById(activeChatId) ? activeChatId : chatSessions[0].id;
|
||||||
|
setChatLoading(false);
|
||||||
return setActiveChat(preferredChatId);
|
return setActiveChat(preferredChatId);
|
||||||
}
|
}
|
||||||
activeChatId = null;
|
activeChatId = null;
|
||||||
@@ -912,21 +1188,29 @@ async function loadChatSessions() {
|
|||||||
renderMessages();
|
renderMessages();
|
||||||
updateWorkspaceCopy();
|
updateWorkspaceCopy();
|
||||||
updateSendState();
|
updateSendState();
|
||||||
|
setChatLoading(false);
|
||||||
return setNotice("Pick a starting action to begin.", "success");
|
return setNotice("Pick a starting action to begin.", "success");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createNewChat() {
|
async function createNewChat() {
|
||||||
|
setChatLoading(true, "Creating chat...");
|
||||||
const res = await post("/chats", {title: "New chat"});
|
const res = await post("/chats", {title: "New chat"});
|
||||||
if (res.error || !res.chat) return setNotice(res.error || "Unable to create chat.", "error");
|
if (res.error || !res.chat) {
|
||||||
|
setChatLoading(false);
|
||||||
|
return setNotice(res.error || "Unable to create chat.", "error");
|
||||||
|
}
|
||||||
chatSessions.unshift(res.chat); await loadSummary(); renderChatList(); await setActiveChat(res.chat.id); setNotice("New chat created.", "success");
|
chatSessions.unshift(res.chat); await loadSummary(); renderChatList(); await setActiveChat(res.chat.id); setNotice("New chat created.", "success");
|
||||||
msgInput.focus();
|
msgInput.focus();
|
||||||
|
setChatLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startRoleplay() {
|
async function startRoleplay() {
|
||||||
|
setChatLoading(true, "Starting roleplay...");
|
||||||
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);
|
||||||
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;
|
||||||
@@ -939,12 +1223,14 @@ async function startRoleplay() {
|
|||||||
});
|
});
|
||||||
roleplayStarterConfirm.disabled = false;
|
roleplayStarterConfirm.disabled = false;
|
||||||
if (res.error || !res.chat) {
|
if (res.error || !res.chat) {
|
||||||
|
setChatLoading(false);
|
||||||
return setRoleplayStarterNotice(res.error || "Unable to start roleplay.", "error");
|
return setRoleplayStarterNotice(res.error || "Unable to start roleplay.", "error");
|
||||||
}
|
}
|
||||||
closeRoleplayStarter();
|
closeRoleplayStarter();
|
||||||
await loadChatSessions();
|
await loadChatSessions();
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renameChat(id) {
|
async function renameChat(id) {
|
||||||
@@ -978,6 +1264,7 @@ async function clearChat(id) {
|
|||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
const res = await post(`/chats/${id}/clear`, {});
|
const res = await post(`/chats/${id}/clear`, {});
|
||||||
if (res.error) return setNotice(res.error, "error");
|
if (res.error) return setNotice(res.error, "error");
|
||||||
|
messageRetryState = new Map();
|
||||||
currentMessages = []; renderMessages(); await loadSummary(); setNotice("Chat cleared.", "success");
|
currentMessages = []; renderMessages(); await loadSummary(); setNotice("Chat cleared.", "success");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -992,8 +1279,12 @@ function exportCurrentChat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deleteChat(id) {
|
async function deleteChat(id) {
|
||||||
|
setChatLoading(true, "Deleting chat...");
|
||||||
const chat = getChatById(id);
|
const chat = getChatById(id);
|
||||||
if (!chat) return;
|
if (!chat) {
|
||||||
|
setChatLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const confirmed = await confirmPopup({
|
const confirmed = await confirmPopup({
|
||||||
eyebrow: "Conversation",
|
eyebrow: "Conversation",
|
||||||
title: "Delete chat",
|
title: "Delete chat",
|
||||||
@@ -1001,14 +1292,22 @@ async function deleteChat(id) {
|
|||||||
confirmLabel: "Delete",
|
confirmLabel: "Delete",
|
||||||
danger: true
|
danger: true
|
||||||
});
|
});
|
||||||
if (!confirmed) return;
|
if (!confirmed) {
|
||||||
|
setChatLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const res = await del(`/chats/${id}`);
|
const res = await del(`/chats/${id}`);
|
||||||
if (res.error) return setNotice(res.error, "error");
|
if (res.error) {
|
||||||
|
setChatLoading(false);
|
||||||
|
return setNotice(res.error, "error");
|
||||||
|
}
|
||||||
chatSessions = chatSessions.filter((item) => item.id !== id);
|
chatSessions = chatSessions.filter((item) => item.id !== id);
|
||||||
if (activeChatId === id) { activeChatId = null; currentMessages = []; renderMessages(); }
|
if (activeChatId === id) { activeChatId = null; currentMessages = []; renderMessages(); }
|
||||||
|
messageRetryState = new Map();
|
||||||
await loadSummary(); renderChatList();
|
await loadSummary(); renderChatList();
|
||||||
if (chatSessions.length) await setActiveChat(chatSessions[0].id); else await createNewChat();
|
if (chatSessions.length) await setActiveChat(chatSessions[0].id); else await createNewChat();
|
||||||
setNotice("Chat deleted.", "success");
|
setNotice("Chat deleted.", "success");
|
||||||
|
setChatLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function displayModels() {
|
async function displayModels() {
|
||||||
@@ -1073,8 +1372,7 @@ async function sendMessage() {
|
|||||||
const res = await post("/chat", {message, model: modelSelect.value, chatId: activeChatId});
|
const res = await post("/chat", {message, model: modelSelect.value, chatId: activeChatId});
|
||||||
if (messagesDiv.contains(loadingMsg)) messagesDiv.removeChild(loadingMsg);
|
if (messagesDiv.contains(loadingMsg)) messagesDiv.removeChild(loadingMsg);
|
||||||
if (res.reply) {
|
if (res.reply) {
|
||||||
currentMessages.push({role: "bot", content: res.reply});
|
await setActiveChat(activeChatId);
|
||||||
addMessage(res.reply);
|
|
||||||
await loadSummary();
|
await loadSummary();
|
||||||
markOnboardingCompleted();
|
markOnboardingCompleted();
|
||||||
maybeShowPersonaNudge();
|
maybeShowPersonaNudge();
|
||||||
@@ -1138,6 +1436,7 @@ registerForm.addEventListener("submit", (event) => { event.preventDefault(); voi
|
|||||||
$("logout").addEventListener("click", async () => {
|
$("logout").addEventListener("click", async () => {
|
||||||
await post("/logout", {}); chatDiv.style.display = "none"; authDiv.style.display = "grid";
|
await post("/logout", {}); chatDiv.style.display = "none"; authDiv.style.display = "grid";
|
||||||
activeChatId = null; currentUsername = ""; currentSummary = null; chatSessions = []; currentMessages = []; assistantPersonas = []; userPersonas = [];
|
activeChatId = null; currentUsername = ""; currentSummary = null; chatSessions = []; currentMessages = []; assistantPersonas = []; userPersonas = [];
|
||||||
|
messageRetryState = new Map();
|
||||||
activeUserPersonaId = null; publishedPersonaIds = new Set(); messagesDiv.innerHTML = ""; renderChatList(); updateChatActionState();
|
activeUserPersonaId = null; publishedPersonaIds = new Set(); messagesDiv.innerHTML = ""; renderChatList(); updateChatActionState();
|
||||||
updateComposerPlaceholder();
|
updateComposerPlaceholder();
|
||||||
updateSendState();
|
updateSendState();
|
||||||
|
|||||||
@@ -1264,6 +1264,35 @@ select:focus {
|
|||||||
opacity: 0.86;
|
opacity: 0.86;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.msg-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-action-btn {
|
||||||
|
min-height: 30px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.26);
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
color: inherit;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-action-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-variant-index {
|
||||||
|
font-size: 0.74rem;
|
||||||
|
opacity: 0.86;
|
||||||
|
}
|
||||||
|
|
||||||
.typing-indicator {
|
.typing-indicator {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1508,6 +1537,10 @@ select:focus {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.market-preview-scenario-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.persona-item-actions button {
|
.persona-item-actions button {
|
||||||
padding: 8px 11px;
|
padding: 8px 11px;
|
||||||
font-size: 0.84rem;
|
font-size: 0.84rem;
|
||||||
@@ -1729,6 +1762,21 @@ select:focus {
|
|||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.market-preview-toggle {
|
||||||
|
margin-top: 6px;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--brand-strong);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-preview-toggle:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
.settings-shell {
|
.settings-shell {
|
||||||
width: min(1180px, 100%);
|
width: min(1180px, 100%);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|||||||
@@ -448,10 +448,10 @@ const insertChatMessageStmt = db.prepare(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const getChatMessagesStmt = db.prepare(`
|
const getChatMessagesStmt = db.prepare(`
|
||||||
SELECT role, content
|
SELECT id, role, content
|
||||||
FROM chat_messages
|
FROM chat_messages
|
||||||
WHERE chat_id = ?
|
WHERE chat_id = ?
|
||||||
ORDER BY datetime(created_at)
|
ORDER BY id
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
;
|
;
|
||||||
@@ -459,12 +459,72 @@ const getRecentChatMessagesStmt = db.prepare(`
|
|||||||
SELECT role, content
|
SELECT role, content
|
||||||
FROM chat_messages
|
FROM chat_messages
|
||||||
WHERE chat_id = ?
|
WHERE chat_id = ?
|
||||||
ORDER BY datetime(created_at) DESC
|
ORDER BY id DESC
|
||||||
LIMIT 20
|
LIMIT 20
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
;
|
;
|
||||||
|
|
||||||
|
const getRecentChatMessagesUpToIdStmt = db.prepare(`
|
||||||
|
SELECT role, content
|
||||||
|
FROM chat_messages
|
||||||
|
WHERE chat_id = ?
|
||||||
|
AND id <= ?
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT 20
|
||||||
|
`
|
||||||
|
)
|
||||||
|
;
|
||||||
|
|
||||||
|
const getChatMessageByIdStmt = db.prepare(`
|
||||||
|
SELECT id, role, content
|
||||||
|
FROM chat_messages
|
||||||
|
WHERE id = ?
|
||||||
|
AND chat_id = ?
|
||||||
|
`
|
||||||
|
)
|
||||||
|
;
|
||||||
|
|
||||||
|
const getChatMessageByIndexStmt = db.prepare(`
|
||||||
|
SELECT id, role, content
|
||||||
|
FROM chat_messages
|
||||||
|
WHERE chat_id = ?
|
||||||
|
ORDER BY id
|
||||||
|
LIMIT 1 OFFSET ?
|
||||||
|
`
|
||||||
|
)
|
||||||
|
;
|
||||||
|
|
||||||
|
const getLatestChatMessageStmt = db.prepare(`
|
||||||
|
SELECT id, role, content
|
||||||
|
FROM chat_messages
|
||||||
|
WHERE chat_id = ?
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT 1
|
||||||
|
`
|
||||||
|
)
|
||||||
|
;
|
||||||
|
|
||||||
|
const getPreviousUserMessageStmt = db.prepare(`
|
||||||
|
SELECT id, role, content
|
||||||
|
FROM chat_messages
|
||||||
|
WHERE chat_id = ?
|
||||||
|
AND role = 'user'
|
||||||
|
AND id < ?
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT 1
|
||||||
|
`
|
||||||
|
)
|
||||||
|
;
|
||||||
|
|
||||||
|
const updateChatMessageStmt = db.prepare(
|
||||||
|
"UPDATE chat_messages SET content = ? WHERE id = ? AND chat_id = ?"
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteChatMessageStmt = db.prepare(
|
||||||
|
"DELETE FROM chat_messages WHERE id = ? AND chat_id = ?"
|
||||||
|
);
|
||||||
|
|
||||||
const deleteChatMessagesStmt = db.prepare(
|
const deleteChatMessagesStmt = db.prepare(
|
||||||
"DELETE FROM chat_messages WHERE chat_id = ?"
|
"DELETE FROM chat_messages WHERE chat_id = ?"
|
||||||
);
|
);
|
||||||
@@ -1072,7 +1132,9 @@ const buildRoleplayDirectionPrompt = ({assistantPersona, userPersona, scenarioPr
|
|||||||
"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.",
|
||||||
"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 or internal thoughts 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.",
|
||||||
|
"The opening must contain only the assistant character's own spoken words and optional self-actions.",
|
||||||
"Prefer a concrete opening beat over vague exposition.",
|
"Prefer a concrete opening beat over vague exposition.",
|
||||||
"Include a subtle hook, tension, invitation, or problem that gives the user something to respond to.",
|
"Include a subtle hook, tension, invitation, or problem that gives the user something to respond to.",
|
||||||
"",
|
"",
|
||||||
@@ -1103,11 +1165,67 @@ 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 only the assistant character's words and optional self-actions.",
|
||||||
|
"Do not write any user dialogue, user thoughts, or user actions.",
|
||||||
|
"Do not use labels such as 'User:' or 'You:'.",
|
||||||
"End with a line, question, action, or reveal that gives the user an obvious way to answer.",
|
"End with a line, question, action, or reveal that gives the user an obvious way to answer.",
|
||||||
`Keep this scene continuity in mind: ${sceneSummary}`
|
`Keep this scene continuity in mind: ${sceneSummary}`
|
||||||
].join("\n");
|
].join("\n");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const escapeRegExp = (value) => String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
|
||||||
|
const containsUserVoiceInRoleplayOpener = (opener, userPersona) => {
|
||||||
|
const text = String(opener || "").trim();
|
||||||
|
if (!text) return true;
|
||||||
|
|
||||||
|
const userName = (userPersona?.name || "").trim();
|
||||||
|
const nameParts = userName ? userName.split(/\s+/).filter(Boolean).slice(0, 2) : [];
|
||||||
|
const tokens = ["user", "you", ...nameParts].filter(Boolean).map(escapeRegExp);
|
||||||
|
if (!tokens.length) return false;
|
||||||
|
|
||||||
|
const labelPattern = new RegExp(`(^|\\n)\\s*(?:${tokens.join("|")})\\s*:`, "i");
|
||||||
|
if (labelPattern.test(text)) return true;
|
||||||
|
|
||||||
|
const userActionPattern = new RegExp(
|
||||||
|
`(^|\\n)\\s*(?:\\*\\s*)?(?:${tokens.join("|")})\\s+` +
|
||||||
|
"(?:say|says|said|ask|asks|asked|reply|replies|replied|think|thinks|thought|feel|feels|felt|walk|walks|walked|step|steps|stepped|look|looks|looked|nod|nods|nodded|smile|smiles|smiled|enter|enters|entered|turn|turns|turned)\\b",
|
||||||
|
"i"
|
||||||
|
);
|
||||||
|
return userActionPattern.test(text);
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateRoleplayOpener = async ({selectedModel = "mistral:latest", assistantPersona, userPersona, scenarioPrompt, sceneSummary}) => {
|
||||||
|
const baseMessages = [
|
||||||
|
{role: "system", content: buildPersonaPrompt(assistantPersona)},
|
||||||
|
...(userPersona ? [{role: "system", content: buildUserPersonaPrompt(userPersona)}] : []),
|
||||||
|
{role: "system", content: buildRoleplayDirectionPrompt({assistantPersona, userPersona, scenarioPrompt, sceneSummary})},
|
||||||
|
{role: "user", content: buildRoleplayOpenerPrompt({assistantPersona, userPersona, scenarioPrompt, sceneSummary})}
|
||||||
|
];
|
||||||
|
|
||||||
|
let opener = await generateModelReply(selectedModel, baseMessages);
|
||||||
|
if (!containsUserVoiceInRoleplayOpener(opener, userPersona)) {
|
||||||
|
return opener;
|
||||||
|
}
|
||||||
|
|
||||||
|
const retryMessages = [
|
||||||
|
...baseMessages,
|
||||||
|
{role: "assistant", content: opener || ""},
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: "Rewrite this opening. Hard rule: only the assistant character may speak or act. Never write user speech, thoughts, actions, or labels like 'User:'/'You:'."
|
||||||
|
}
|
||||||
|
];
|
||||||
|
opener = await generateModelReply(selectedModel, retryMessages);
|
||||||
|
return opener;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getChatMessageByIndex = (chatId, index) => {
|
||||||
|
const safeIndex = Number(index);
|
||||||
|
if (!Number.isInteger(safeIndex) || safeIndex < 0) return null;
|
||||||
|
return getChatMessageByIndexStmt.get(chatId, safeIndex) || null;
|
||||||
|
};
|
||||||
|
|
||||||
const createPersonaChat = (user, persona, userPersonaId = null, scenarioPrompt = null, scenarioSummary = null) => {
|
const createPersonaChat = (user, persona, userPersonaId = null, scenarioPrompt = null, scenarioSummary = null) => {
|
||||||
const chatId = insertChatSessionStmt.run(user, persona.name, persona.id, userPersonaId, scenarioPrompt, scenarioSummary).lastInsertRowid;
|
const chatId = insertChatSessionStmt.run(user, persona.name, persona.id, userPersonaId, scenarioPrompt, scenarioSummary).lastInsertRowid;
|
||||||
return getChatSessionStmt.get(chatId, user);
|
return getChatSessionStmt.get(chatId, user);
|
||||||
@@ -1270,6 +1388,80 @@ app.get("/chats/:id/messages", requireLogin, (req, res) => {
|
|||||||
res.json({messages, chat: session});
|
res.json({messages, chat: session});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.put("/chats/:id/messages/:messageId", requireLogin, (req, res) => {
|
||||||
|
const user = req.session.user;
|
||||||
|
const chatId = Number(req.params.id);
|
||||||
|
const messageId = Number(req.params.messageId);
|
||||||
|
const content = String(req.body?.content || "").trim();
|
||||||
|
const session = getChatSessionStmt.get(chatId, user);
|
||||||
|
if (!session) {
|
||||||
|
return res.status(404).json({error: "Chat not found"});
|
||||||
|
}
|
||||||
|
if (!content) {
|
||||||
|
return res.status(400).json({error: "Message content is required"});
|
||||||
|
}
|
||||||
|
const result = updateChatMessageStmt.run(content, messageId, chatId);
|
||||||
|
if (result.changes === 0) {
|
||||||
|
return res.status(404).json({error: "Message not found"});
|
||||||
|
}
|
||||||
|
touchChatSessionStmt.run(chatId, user);
|
||||||
|
const message = getChatMessageByIdStmt.get(messageId, chatId);
|
||||||
|
res.json({message});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.put("/chats/:id/messages/by-index/:index", requireLogin, (req, res) => {
|
||||||
|
const user = req.session.user;
|
||||||
|
const chatId = Number(req.params.id);
|
||||||
|
const content = String(req.body?.content || "").trim();
|
||||||
|
const session = getChatSessionStmt.get(chatId, user);
|
||||||
|
if (!session) {
|
||||||
|
return res.status(404).json({error: "Chat not found"});
|
||||||
|
}
|
||||||
|
if (!content) {
|
||||||
|
return res.status(400).json({error: "Message content is required"});
|
||||||
|
}
|
||||||
|
const target = getChatMessageByIndex(chatId, req.params.index);
|
||||||
|
if (!target) {
|
||||||
|
return res.status(404).json({error: "Message not found"});
|
||||||
|
}
|
||||||
|
updateChatMessageStmt.run(content, target.id, chatId);
|
||||||
|
touchChatSessionStmt.run(chatId, user);
|
||||||
|
const message = getChatMessageByIdStmt.get(target.id, chatId);
|
||||||
|
res.json({message});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete("/chats/:id/messages/:messageId", requireLogin, (req, res) => {
|
||||||
|
const user = req.session.user;
|
||||||
|
const chatId = Number(req.params.id);
|
||||||
|
const messageId = Number(req.params.messageId);
|
||||||
|
const session = getChatSessionStmt.get(chatId, user);
|
||||||
|
if (!session) {
|
||||||
|
return res.status(404).json({error: "Chat not found"});
|
||||||
|
}
|
||||||
|
const result = deleteChatMessageStmt.run(messageId, chatId);
|
||||||
|
if (result.changes === 0) {
|
||||||
|
return res.status(404).json({error: "Message not found"});
|
||||||
|
}
|
||||||
|
touchChatSessionStmt.run(chatId, user);
|
||||||
|
res.json({message: "Message deleted"});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete("/chats/:id/messages/by-index/:index", requireLogin, (req, res) => {
|
||||||
|
const user = req.session.user;
|
||||||
|
const chatId = Number(req.params.id);
|
||||||
|
const session = getChatSessionStmt.get(chatId, user);
|
||||||
|
if (!session) {
|
||||||
|
return res.status(404).json({error: "Chat not found"});
|
||||||
|
}
|
||||||
|
const target = getChatMessageByIndex(chatId, req.params.index);
|
||||||
|
if (!target) {
|
||||||
|
return res.status(404).json({error: "Message not found"});
|
||||||
|
}
|
||||||
|
deleteChatMessageStmt.run(target.id, chatId);
|
||||||
|
touchChatSessionStmt.run(chatId, user);
|
||||||
|
res.json({message: "Message deleted"});
|
||||||
|
});
|
||||||
|
|
||||||
app.post("/chats/:id/clear", requireLogin, (req, res) => {
|
app.post("/chats/:id/clear", requireLogin, (req, res) => {
|
||||||
const user = req.session.user;
|
const user = req.session.user;
|
||||||
const chatId = Number(req.params.id);
|
const chatId = Number(req.params.id);
|
||||||
@@ -1511,7 +1703,7 @@ app.post("/personas/market/:id/collect", requireLogin, (req, res) => {
|
|||||||
res.json({persona, activeUserPersonaId});
|
res.json({persona, activeUserPersonaId});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/personas/market/:id/chat", requireLogin, (req, res) => {
|
app.post("/personas/market/:id/chat", requireLogin, async (req, res) => {
|
||||||
const user = req.session.user;
|
const user = req.session.user;
|
||||||
const marketId = Number(req.params.id);
|
const marketId = Number(req.params.id);
|
||||||
const userPersonaId = req.body?.userPersonaId ? Number(req.body.userPersonaId) : null;
|
const userPersonaId = req.body?.userPersonaId ? Number(req.body.userPersonaId) : null;
|
||||||
@@ -1565,18 +1757,23 @@ app.post("/personas/market/:id/chat", requireLogin, (req, res) => {
|
|||||||
}
|
}
|
||||||
updateChatSessionSceneStmt.run(scenarioPrompt, sceneSummary, chat.id, user);
|
updateChatSessionSceneStmt.run(scenarioPrompt, sceneSummary, chat.id, user);
|
||||||
chat = getChatSessionStmt.get(chat.id, user);
|
chat = getChatSessionStmt.get(chat.id, user);
|
||||||
Promise.resolve(generateModelReply("mistral:latest", [
|
try {
|
||||||
{role: "system", content: buildPersonaPrompt(persona)},
|
const opener = await generateRoleplayOpener({
|
||||||
...(userPersona ? [{role: "system", content: buildUserPersonaPrompt(userPersona)}] : []),
|
selectedModel: "mistral:latest",
|
||||||
{role: "system", content: buildRoleplayDirectionPrompt({assistantPersona: persona, userPersona, scenarioPrompt, sceneSummary})},
|
assistantPersona: persona,
|
||||||
{role: "user", content: buildRoleplayOpenerPrompt({assistantPersona: persona, userPersona, scenarioPrompt, sceneSummary})}
|
userPersona,
|
||||||
])).then((opener) => {
|
scenarioPrompt,
|
||||||
|
sceneSummary
|
||||||
|
});
|
||||||
|
if (!opener || containsUserVoiceInRoleplayOpener(opener, userPersona)) {
|
||||||
|
return res.json({persona, chat, existing: false, generatedInitialMessage: false});
|
||||||
|
}
|
||||||
insertChatMessageStmt.run(chat.id, "bot", opener);
|
insertChatMessageStmt.run(chat.id, "bot", opener);
|
||||||
touchChatSessionStmt.run(chat.id, user);
|
touchChatSessionStmt.run(chat.id, user);
|
||||||
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 {
|
||||||
res.json({persona, chat, existing: false, generatedInitialMessage: false});
|
return res.json({persona, chat, existing: false, generatedInitialMessage: false});
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/roleplays/start", requireLogin, async (req, res) => {
|
app.post("/roleplays/start", requireLogin, async (req, res) => {
|
||||||
@@ -1621,24 +1818,15 @@ app.post("/roleplays/start", requireLogin, async (req, res) => {
|
|||||||
updateChatSessionSceneStmt.run(scenarioPrompt, sceneSummary, chat.id, user);
|
updateChatSessionSceneStmt.run(scenarioPrompt, sceneSummary, chat.id, user);
|
||||||
chat = getChatSessionStmt.get(chat.id, user);
|
chat = getChatSessionStmt.get(chat.id, user);
|
||||||
|
|
||||||
const messagesPayload = [
|
|
||||||
{role: "system", content: buildPersonaPrompt(assistantPersona)}
|
|
||||||
];
|
|
||||||
if (userPersona) {
|
|
||||||
messagesPayload.push({role: "system", content: buildUserPersonaPrompt(userPersona)});
|
|
||||||
}
|
|
||||||
messagesPayload.push({
|
|
||||||
role: "system",
|
|
||||||
content: buildRoleplayDirectionPrompt({assistantPersona, userPersona, scenarioPrompt, sceneSummary})
|
|
||||||
});
|
|
||||||
messagesPayload.push({
|
|
||||||
role: "user",
|
|
||||||
content: buildRoleplayOpenerPrompt({assistantPersona, userPersona, scenarioPrompt, sceneSummary})
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const opener = await generateModelReply(selectedModel, messagesPayload);
|
const opener = await generateRoleplayOpener({
|
||||||
if (!opener) {
|
selectedModel,
|
||||||
|
assistantPersona,
|
||||||
|
userPersona,
|
||||||
|
scenarioPrompt,
|
||||||
|
sceneSummary
|
||||||
|
});
|
||||||
|
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);
|
insertChatMessageStmt.run(chat.id, "bot", opener);
|
||||||
@@ -1715,6 +1903,151 @@ app.post("/chat", requireLogin, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.post("/chats/:id/messages/:messageId/retry", requireLogin, async (req, res) => {
|
||||||
|
const user = req.session.user;
|
||||||
|
const chatId = Number(req.params.id);
|
||||||
|
const messageId = Number(req.params.messageId);
|
||||||
|
const selectedModel = req.body?.model || "mistral:latest";
|
||||||
|
const session = getChatSessionStmt.get(chatId, user);
|
||||||
|
if (!session) {
|
||||||
|
return res.status(404).json({error: "Chat not found"});
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetMessage = getChatMessageByIdStmt.get(messageId, chatId);
|
||||||
|
if (!targetMessage) {
|
||||||
|
return res.status(404).json({error: "Message not found"});
|
||||||
|
}
|
||||||
|
const latestMessage = getLatestChatMessageStmt.get(chatId);
|
||||||
|
if (!latestMessage || latestMessage.id !== messageId || latestMessage.role !== "bot") {
|
||||||
|
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 {
|
||||||
|
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 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 res.status(500).json({error: "Failed to regenerate message"});
|
||||||
|
}
|
||||||
|
updateChatMessageStmt.run(fullReply, messageId, chatId);
|
||||||
|
touchChatSessionStmt.run(chatId, user);
|
||||||
|
return res.json({message: {id: messageId, role: "bot", content: fullReply}, promptMessageId: promptMessage.id});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Retry error:", err);
|
||||||
|
return res.status(500).json({error: "Failed to regenerate message"});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/chats/:id/messages/by-index/:index/retry", requireLogin, async (req, res) => {
|
||||||
|
const user = req.session.user;
|
||||||
|
const chatId = Number(req.params.id);
|
||||||
|
const selectedModel = req.body?.model || "mistral:latest";
|
||||||
|
const session = getChatSessionStmt.get(chatId, user);
|
||||||
|
if (!session) {
|
||||||
|
return res.status(404).json({error: "Chat not found"});
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetMessage = getChatMessageByIndex(chatId, req.params.index);
|
||||||
|
if (!targetMessage) {
|
||||||
|
return res.status(404).json({error: "Message not found"});
|
||||||
|
}
|
||||||
|
const latestMessage = getLatestChatMessageStmt.get(chatId);
|
||||||
|
if (!latestMessage || latestMessage.id !== targetMessage.id || latestMessage.role !== "bot") {
|
||||||
|
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 {
|
||||||
|
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 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 res.status(500).json({error: "Failed to regenerate message"});
|
||||||
|
}
|
||||||
|
updateChatMessageStmt.run(fullReply, targetMessage.id, chatId);
|
||||||
|
touchChatSessionStmt.run(chatId, user);
|
||||||
|
return res.json({message: {id: targetMessage.id, role: "bot", content: fullReply}, promptMessageId: promptMessage.id});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Retry error:", err);
|
||||||
|
return res.status(500).json({error: "Failed to regenerate message"});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.get("/session", (req, res) => {
|
app.get("/session", (req, res) => {
|
||||||
if (req.session.user) res.json({user: req.session.user});
|
if (req.session.user) res.json({user: req.session.user});
|
||||||
else res.json({user: null});
|
else res.json({user: null});
|
||||||
|
|||||||
Reference in New Issue
Block a user