add more features
This commit is contained in:
@@ -216,7 +216,6 @@
|
||||
<div class="input-shell input-shell-focus">
|
||||
<div class="input-meta">
|
||||
<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 id="quickPrompts" class="quick-prompts" aria-label="Suggested starters"></div>
|
||||
<div class="input-container">
|
||||
|
||||
@@ -108,6 +108,10 @@
|
||||
<span>You in this roleplay</span>
|
||||
<select id="marketPreviewUserPersonaSelect"></select>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+53
-8
@@ -18,6 +18,8 @@ const marketPreviewDetails = $("marketPreviewDetails");
|
||||
const marketPreviewExampleDialogues = $("marketPreviewExampleDialogues");
|
||||
const marketPreviewUserPersonaField = $("marketPreviewUserPersonaField");
|
||||
const marketPreviewUserPersonaSelect = $("marketPreviewUserPersonaSelect");
|
||||
const marketPreviewScenarioField = $("marketPreviewScenarioField");
|
||||
const marketPreviewScenario = $("marketPreviewScenario");
|
||||
const marketPreviewConfirm = $("marketPreviewConfirm");
|
||||
const marketPreviewClose = $("marketPreviewClose");
|
||||
const marketPersonaModal = $("marketPersonaModal");
|
||||
@@ -112,10 +114,45 @@ function formatPersonaField(value) {
|
||||
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() {
|
||||
marketPreviewModal.classList.add("hidden");
|
||||
pendingMarketPersona = null;
|
||||
marketPreviewUserPersonaSelect.innerHTML = "";
|
||||
marketPreviewScenario.value = "";
|
||||
}
|
||||
|
||||
function populateUserPersonaChoices() {
|
||||
@@ -151,14 +188,15 @@ function openMarketPreview(persona) {
|
||||
pendingMarketPersona = persona;
|
||||
marketPreviewName.textContent = persona.name;
|
||||
marketPreviewMeta.textContent = buildPersonaMeta(persona);
|
||||
marketPreviewPronouns.textContent = formatPersonaField(persona.pronouns);
|
||||
marketPreviewAppearance.textContent = formatPersonaField(persona.appearance);
|
||||
marketPreviewBackground.textContent = formatPersonaField(persona.background);
|
||||
marketPreviewDetails.textContent = formatPersonaField(persona.details);
|
||||
marketPreviewExampleDialogues.textContent = formatPersonaField(persona.example_dialogues);
|
||||
setCollapsiblePreviewText(marketPreviewPronouns, persona.pronouns, 80);
|
||||
setCollapsiblePreviewText(marketPreviewAppearance, persona.appearance);
|
||||
setCollapsiblePreviewText(marketPreviewBackground, persona.background);
|
||||
setCollapsiblePreviewText(marketPreviewDetails, persona.details);
|
||||
setCollapsiblePreviewText(marketPreviewExampleDialogues, persona.example_dialogues);
|
||||
|
||||
const isAssistant = persona.persona_type === "assistant";
|
||||
marketPreviewUserPersonaField.classList.toggle("hidden", !isAssistant);
|
||||
marketPreviewScenarioField.classList.toggle("hidden", !isAssistant);
|
||||
marketPreviewConfirm.textContent = isAssistant ? "Start roleplay" : "Collect persona";
|
||||
if (isAssistant) {
|
||||
populateUserPersonaChoices();
|
||||
@@ -303,7 +341,7 @@ async function collectMarketPersona(marketId) {
|
||||
setMarketStatus("User Persona collected.", "success");
|
||||
}
|
||||
|
||||
async function startMarketPersonaChat(marketId, userPersonaSelection) {
|
||||
async function startMarketPersonaChat(marketId, userPersonaSelection, scenarioPrompt = "") {
|
||||
if (!userPersonaSelection) {
|
||||
setMarketStatus("Choose who you are before starting roleplay.", "error");
|
||||
return;
|
||||
@@ -315,7 +353,10 @@ async function startMarketPersonaChat(marketId, userPersonaSelection) {
|
||||
|
||||
setMarketStatus("Starting AI Character chat...");
|
||||
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) {
|
||||
setMarketStatus(res.error || "Unable to start chat.", "error");
|
||||
return;
|
||||
@@ -385,7 +426,11 @@ marketPreviewConfirm.addEventListener("click", async () => {
|
||||
if (!pendingMarketPersona) return;
|
||||
const marketId = pendingMarketPersona.id;
|
||||
if (pendingMarketPersona.persona_type === "assistant") {
|
||||
await startMarketPersonaChat(marketId, marketPreviewUserPersonaSelect.value || "self");
|
||||
await startMarketPersonaChat(
|
||||
marketId,
|
||||
marketPreviewUserPersonaSelect.value || "self",
|
||||
marketPreviewScenario.value || ""
|
||||
);
|
||||
closeMarketPreview();
|
||||
return;
|
||||
}
|
||||
|
||||
+322
-23
@@ -10,6 +10,7 @@ const registerUsernameInput = $("registerUsername"), registerPasswordInput = $("
|
||||
const loginSubmit = $("loginSubmit"), registerSubmit = $("registerSubmit");
|
||||
const chatList = $("chatList"), chatSearchInput = $("chatSearch"), newChatBtn = $("newChat");
|
||||
const startRoleplayBtn = $("startRoleplay");
|
||||
const chatListLoading = $("chatListLoading"), chatListLoadingText = $("chatListLoadingText");
|
||||
const renameChatBtn = $("renameChat"), clearChatBtn = $("clearChat"), exportChatBtn = $("exportChat");
|
||||
const activeChatTitle = $("activeChatTitle"), sessionUser = $("sessionUser");
|
||||
const contextChipButton = $("contextChipButton"), contextPopover = $("contextPopover"), contextSummaryLabel = $("contextSummaryLabel");
|
||||
@@ -46,6 +47,8 @@ let roleplayPresetCharacterId = null;
|
||||
let noticeTimer = null;
|
||||
let workspaceMode = localStorage.getItem("krishd-workspace-mode") || "basic";
|
||||
let onboardingStep = 1, onboardingIntent = "ask";
|
||||
let chatLoadingDepth = 0;
|
||||
let messageRetryState = new Map();
|
||||
|
||||
const onboardingPrompts = {
|
||||
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";
|
||||
}
|
||||
|
||||
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) {
|
||||
workspaceMode = nextMode === "advanced" ? "advanced" : "basic";
|
||||
document.body.dataset.workspaceMode = workspaceMode;
|
||||
@@ -245,16 +263,6 @@ function updateRoleplaySuggestions() {
|
||||
});
|
||||
}
|
||||
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 [];
|
||||
}
|
||||
function renderQuickPrompts() {
|
||||
@@ -295,7 +303,195 @@ function updateSendState() {
|
||||
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");
|
||||
msgDiv.className = `msg ${isUser ? "user" : "bot"}${isLoading ? " loading" : ""}`;
|
||||
if (isLoading) {
|
||||
@@ -312,12 +508,78 @@ function addMessage(content, isUser = false, isLoading = false) {
|
||||
</div>
|
||||
</div>`;
|
||||
} 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";
|
||||
actions.className = "msg-actions";
|
||||
const activeChat = getChatById(activeChatId);
|
||||
header.textContent = isUser ? "You" : (activeChat?.assistant_persona_name || "Assistant");
|
||||
setMessageContent(body, content);
|
||||
header.textContent = isMessageUser ? "You" : (activeChat?.assistant_persona_name || "Assistant");
|
||||
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);
|
||||
if (actions.childElementCount) {
|
||||
msgDiv.appendChild(actions);
|
||||
}
|
||||
}
|
||||
messagesDiv.appendChild(msgDiv);
|
||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||
@@ -330,7 +592,10 @@ function renderMessages() {
|
||||
if (!currentMessages.length && (!chat || chat.assistant_persona_id)) {
|
||||
renderMessageEmptyState();
|
||||
} 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();
|
||||
updateChatActionState();
|
||||
@@ -889,22 +1154,33 @@ function showAuthScreen(target) {
|
||||
}
|
||||
|
||||
async function setActiveChat(id) {
|
||||
setChatLoading(true, "Opening chat...");
|
||||
activeChatId = id;
|
||||
messageRetryState = new Map();
|
||||
updateWorkspaceCopy(); renderChatList(); updateChatActionState();
|
||||
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);
|
||||
if (res.chat && existing) Object.assign(existing, res.chat);
|
||||
currentMessages = res.messages || []; renderMessages();
|
||||
updateSendState();
|
||||
setChatLoading(false);
|
||||
}
|
||||
|
||||
async function loadChatSessions() {
|
||||
setChatLoading(true, "Loading 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();
|
||||
if (chatSessions.length) {
|
||||
const preferredChatId = (requestedChatId && getChatById(requestedChatId)) ? requestedChatId : activeChatId && getChatById(activeChatId) ? activeChatId : chatSessions[0].id;
|
||||
setChatLoading(false);
|
||||
return setActiveChat(preferredChatId);
|
||||
}
|
||||
activeChatId = null;
|
||||
@@ -912,21 +1188,29 @@ async function loadChatSessions() {
|
||||
renderMessages();
|
||||
updateWorkspaceCopy();
|
||||
updateSendState();
|
||||
setChatLoading(false);
|
||||
return setNotice("Pick a starting action to begin.", "success");
|
||||
}
|
||||
|
||||
async function createNewChat() {
|
||||
setChatLoading(true, "Creating 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");
|
||||
msgInput.focus();
|
||||
setChatLoading(false);
|
||||
}
|
||||
|
||||
async function startRoleplay() {
|
||||
setChatLoading(true, "Starting roleplay...");
|
||||
const assistantPersonaId = Number(roleplayCharacterSelect.value);
|
||||
const userPersonaId = roleplayUserPersonaSelect.value ? Number(roleplayUserPersonaSelect.value) : null;
|
||||
const scenarioPrompt = roleplayScenarioInput?.value.trim() || "";
|
||||
if (!assistantPersonaId) {
|
||||
setChatLoading(false);
|
||||
return setRoleplayStarterNotice("Choose a character to start the roleplay.", "error");
|
||||
}
|
||||
roleplayStarterConfirm.disabled = true;
|
||||
@@ -939,12 +1223,14 @@ async function startRoleplay() {
|
||||
});
|
||||
roleplayStarterConfirm.disabled = false;
|
||||
if (res.error || !res.chat) {
|
||||
setChatLoading(false);
|
||||
return setRoleplayStarterNotice(res.error || "Unable to start roleplay.", "error");
|
||||
}
|
||||
closeRoleplayStarter();
|
||||
await loadChatSessions();
|
||||
await setActiveChat(res.chat.id);
|
||||
setNotice(res.generatedInitialMessage ? "Roleplay started with a fresh scene." : "Roleplay reopened.", "success");
|
||||
setChatLoading(false);
|
||||
}
|
||||
|
||||
async function renameChat(id) {
|
||||
@@ -978,6 +1264,7 @@ async function clearChat(id) {
|
||||
if (!confirmed) return;
|
||||
const res = await post(`/chats/${id}/clear`, {});
|
||||
if (res.error) return setNotice(res.error, "error");
|
||||
messageRetryState = new Map();
|
||||
currentMessages = []; renderMessages(); await loadSummary(); setNotice("Chat cleared.", "success");
|
||||
}
|
||||
|
||||
@@ -992,8 +1279,12 @@ function exportCurrentChat() {
|
||||
}
|
||||
|
||||
async function deleteChat(id) {
|
||||
setChatLoading(true, "Deleting chat...");
|
||||
const chat = getChatById(id);
|
||||
if (!chat) return;
|
||||
if (!chat) {
|
||||
setChatLoading(false);
|
||||
return;
|
||||
}
|
||||
const confirmed = await confirmPopup({
|
||||
eyebrow: "Conversation",
|
||||
title: "Delete chat",
|
||||
@@ -1001,14 +1292,22 @@ async function deleteChat(id) {
|
||||
confirmLabel: "Delete",
|
||||
danger: true
|
||||
});
|
||||
if (!confirmed) return;
|
||||
if (!confirmed) {
|
||||
setChatLoading(false);
|
||||
return;
|
||||
}
|
||||
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);
|
||||
if (activeChatId === id) { activeChatId = null; currentMessages = []; renderMessages(); }
|
||||
messageRetryState = new Map();
|
||||
await loadSummary(); renderChatList();
|
||||
if (chatSessions.length) await setActiveChat(chatSessions[0].id); else await createNewChat();
|
||||
setNotice("Chat deleted.", "success");
|
||||
setChatLoading(false);
|
||||
}
|
||||
|
||||
async function displayModels() {
|
||||
@@ -1073,8 +1372,7 @@ async function sendMessage() {
|
||||
const res = await post("/chat", {message, model: modelSelect.value, chatId: activeChatId});
|
||||
if (messagesDiv.contains(loadingMsg)) messagesDiv.removeChild(loadingMsg);
|
||||
if (res.reply) {
|
||||
currentMessages.push({role: "bot", content: res.reply});
|
||||
addMessage(res.reply);
|
||||
await setActiveChat(activeChatId);
|
||||
await loadSummary();
|
||||
markOnboardingCompleted();
|
||||
maybeShowPersonaNudge();
|
||||
@@ -1138,6 +1436,7 @@ registerForm.addEventListener("submit", (event) => { event.preventDefault(); voi
|
||||
$("logout").addEventListener("click", async () => {
|
||||
await post("/logout", {}); chatDiv.style.display = "none"; authDiv.style.display = "grid";
|
||||
activeChatId = null; currentUsername = ""; currentSummary = null; chatSessions = []; currentMessages = []; assistantPersonas = []; userPersonas = [];
|
||||
messageRetryState = new Map();
|
||||
activeUserPersonaId = null; publishedPersonaIds = new Set(); messagesDiv.innerHTML = ""; renderChatList(); updateChatActionState();
|
||||
updateComposerPlaceholder();
|
||||
updateSendState();
|
||||
|
||||
@@ -1264,6 +1264,35 @@ select:focus {
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1508,6 +1537,10 @@ select:focus {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.market-preview-scenario-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.persona-item-actions button {
|
||||
padding: 8px 11px;
|
||||
font-size: 0.84rem;
|
||||
@@ -1729,6 +1762,21 @@ select:focus {
|
||||
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 {
|
||||
width: min(1180px, 100%);
|
||||
margin: 0 auto;
|
||||
|
||||
@@ -448,10 +448,10 @@ const insertChatMessageStmt = db.prepare(
|
||||
);
|
||||
|
||||
const getChatMessagesStmt = db.prepare(`
|
||||
SELECT role, content
|
||||
SELECT id, role, content
|
||||
FROM chat_messages
|
||||
WHERE chat_id = ?
|
||||
ORDER BY datetime(created_at)
|
||||
ORDER BY id
|
||||
`
|
||||
)
|
||||
;
|
||||
@@ -459,12 +459,72 @@ const getRecentChatMessagesStmt = db.prepare(`
|
||||
SELECT role, content
|
||||
FROM chat_messages
|
||||
WHERE chat_id = ?
|
||||
ORDER BY datetime(created_at) DESC
|
||||
ORDER BY id DESC
|
||||
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(
|
||||
"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.",
|
||||
"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.",
|
||||
"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.",
|
||||
"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.`,
|
||||
"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.",
|
||||
"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.",
|
||||
`Keep this scene continuity in mind: ${sceneSummary}`
|
||||
].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 chatId = insertChatSessionStmt.run(user, persona.name, persona.id, userPersonaId, scenarioPrompt, scenarioSummary).lastInsertRowid;
|
||||
return getChatSessionStmt.get(chatId, user);
|
||||
@@ -1270,6 +1388,80 @@ app.get("/chats/:id/messages", requireLogin, (req, res) => {
|
||||
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) => {
|
||||
const user = req.session.user;
|
||||
const chatId = Number(req.params.id);
|
||||
@@ -1511,7 +1703,7 @@ app.post("/personas/market/:id/collect", requireLogin, (req, res) => {
|
||||
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 marketId = Number(req.params.id);
|
||||
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);
|
||||
chat = getChatSessionStmt.get(chat.id, user);
|
||||
Promise.resolve(generateModelReply("mistral:latest", [
|
||||
{role: "system", content: buildPersonaPrompt(persona)},
|
||||
...(userPersona ? [{role: "system", content: buildUserPersonaPrompt(userPersona)}] : []),
|
||||
{role: "system", content: buildRoleplayDirectionPrompt({assistantPersona: persona, userPersona, scenarioPrompt, sceneSummary})},
|
||||
{role: "user", content: buildRoleplayOpenerPrompt({assistantPersona: persona, userPersona, scenarioPrompt, sceneSummary})}
|
||||
])).then((opener) => {
|
||||
try {
|
||||
const opener = await generateRoleplayOpener({
|
||||
selectedModel: "mistral:latest",
|
||||
assistantPersona: persona,
|
||||
userPersona,
|
||||
scenarioPrompt,
|
||||
sceneSummary
|
||||
});
|
||||
if (!opener || containsUserVoiceInRoleplayOpener(opener, userPersona)) {
|
||||
return res.json({persona, chat, existing: false, generatedInitialMessage: false});
|
||||
}
|
||||
insertChatMessageStmt.run(chat.id, "bot", opener);
|
||||
touchChatSessionStmt.run(chat.id, user);
|
||||
res.json({persona, chat: getChatSessionStmt.get(chat.id, user), existing: false, generatedInitialMessage: true, opener});
|
||||
}).catch(() => {
|
||||
res.json({persona, chat, existing: false, generatedInitialMessage: false});
|
||||
});
|
||||
return res.json({persona, chat: getChatSessionStmt.get(chat.id, user), existing: false, generatedInitialMessage: true, opener});
|
||||
} catch {
|
||||
return res.json({persona, chat, existing: false, generatedInitialMessage: false});
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
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 {
|
||||
const opener = await generateModelReply(selectedModel, messagesPayload);
|
||||
if (!opener) {
|
||||
const opener = await generateRoleplayOpener({
|
||||
selectedModel,
|
||||
assistantPersona,
|
||||
userPersona,
|
||||
scenarioPrompt,
|
||||
sceneSummary
|
||||
});
|
||||
if (!opener || containsUserVoiceInRoleplayOpener(opener, userPersona)) {
|
||||
return res.status(500).json({error: "Failed to generate the character 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) => {
|
||||
if (req.session.user) res.json({user: req.session.user});
|
||||
else res.json({user: null});
|
||||
|
||||
Reference in New Issue
Block a user