try to make app more intuitive

This commit is contained in:
2026-04-09 22:53:19 +02:00
parent 602e29e54b
commit 057f59ceef
6 changed files with 344 additions and 39 deletions
+9 -1
View File
@@ -113,6 +113,7 @@
<span id="chatModePill" class="context-pill chat-mode-pill">Assistant mode</span> <span id="chatModePill" class="context-pill chat-mode-pill">Assistant mode</span>
<span id="chatCharacterPill" class="context-pill hidden">Character: none</span> <span id="chatCharacterPill" class="context-pill hidden">Character: none</span>
<span id="chatUserPersonaPill" class="context-pill hidden">User persona: none</span> <span id="chatUserPersonaPill" class="context-pill hidden">User persona: none</span>
<span id="chatScenePill" class="context-pill hidden">Scene: none</span>
</div> </div>
</div> </div>
<div class="chat-toolbar"> <div class="chat-toolbar">
@@ -179,7 +180,9 @@
<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 sends. Shift+Enter adds a new line.</p> <p class="composer-hint">Enter sends. Shift+Enter adds a 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 class="input-container"> <div class="input-container">
<textarea id="msgInput" placeholder="Type your message..."></textarea> <textarea id="msgInput" placeholder="Type your message..."></textarea>
<button id="send" type="button" aria-label="Send message"></button> <button id="send" type="button" aria-label="Send message"></button>
@@ -264,7 +267,7 @@
</div> </div>
<p id="roleplayStarterNotice" class="status hidden"></p> <p id="roleplayStarterNotice" class="status hidden"></p>
<div class="roleplay-starter-actions"> <div class="roleplay-starter-actions">
<button id="roleplayNewPersona" class="secondary-action" type="button">New persona</button> <button id="roleplayNewPersona" class="secondary-action" type="button">Create AI Character</button>
<a href="/settings" class="secondary-action">Settings</a> <a href="/settings" class="secondary-action">Settings</a>
</div> </div>
<div class="persona-form-grid"> <div class="persona-form-grid">
@@ -276,7 +279,12 @@
<span>User persona</span> <span>User persona</span>
<select id="roleplayUserPersonaSelect"></select> <select id="roleplayUserPersonaSelect"></select>
</label> </label>
<label class="field field-wide">
<span>Scenario seed</span>
<textarea id="roleplayScenarioInput" rows="3" placeholder="Optional. Describe the kind of opening scene you want, such as a setting, conflict, relationship dynamic, or mood."></textarea>
</label>
</div> </div>
<div id="roleplayStarterSuggestions" class="quick-prompts" aria-label="Scenario suggestions"></div>
<div class="popup-actions"> <div class="popup-actions">
<button id="roleplayStarterCancel" type="button" class="secondary-action">Cancel</button> <button id="roleplayStarterCancel" type="button" class="secondary-action">Cancel</button>
<button id="roleplayStarterConfirm" type="button">Start roleplay</button> <button id="roleplayStarterConfirm" type="button">Start roleplay</button>
+133 -9
View File
@@ -11,7 +11,7 @@ const loginSubmit = $("loginSubmit"), registerSubmit = $("registerSubmit");
const chatList = $("chatList"), chatSearchInput = $("chatSearch"), newChatBtn = $("newChat"); const chatList = $("chatList"), chatSearchInput = $("chatSearch"), newChatBtn = $("newChat");
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 chatModePill = $("chatModePill"), chatCharacterPill = $("chatCharacterPill"), chatUserPersonaPill = $("chatUserPersonaPill"); const chatModePill = $("chatModePill"), chatCharacterPill = $("chatCharacterPill"), chatUserPersonaPill = $("chatUserPersonaPill"), chatScenePill = $("chatScenePill");
const personaList = $("personaList"), userPersonaList = $("userPersonaList"); const personaList = $("personaList"), userPersonaList = $("userPersonaList");
const personaForm = $("personaForm"), personaFormTitle = $("personaFormTitle"), personaTypeSelect = $("personaType"); const personaForm = $("personaForm"), personaFormTitle = $("personaFormTitle"), personaTypeSelect = $("personaType");
const personaNameInput = $("personaName"), personaPronounsInput = $("personaPronouns"), personaAppearanceInput = $("personaAppearance"); const personaNameInput = $("personaName"), personaPronounsInput = $("personaPronouns"), personaAppearanceInput = $("personaAppearance");
@@ -23,9 +23,11 @@ const activePersonaStatus = $("activePersonaStatus"), activeUserPersonaStatus =
const personaModal = $("personaModal"), personaCloseBtn = $("personaClose"), personaMenuButton = $("personaMenuButton"), personaPopover = $("personaPopover"); const personaModal = $("personaModal"), personaCloseBtn = $("personaClose"), personaMenuButton = $("personaMenuButton"), personaPopover = $("personaPopover");
const roleplayStarterModal = $("roleplayStarterModal"), roleplayStarterClose = $("roleplayStarterClose"), roleplayStarterCancel = $("roleplayStarterCancel"), roleplayStarterConfirm = $("roleplayStarterConfirm"); const roleplayStarterModal = $("roleplayStarterModal"), roleplayStarterClose = $("roleplayStarterClose"), roleplayStarterCancel = $("roleplayStarterCancel"), roleplayStarterConfirm = $("roleplayStarterConfirm");
const roleplayStarterNotice = $("roleplayStarterNotice"), roleplayCharacterSelect = $("roleplayCharacterSelect"), roleplayUserPersonaSelect = $("roleplayUserPersonaSelect"); const roleplayStarterNotice = $("roleplayStarterNotice"), roleplayCharacterSelect = $("roleplayCharacterSelect"), roleplayUserPersonaSelect = $("roleplayUserPersonaSelect");
const roleplayScenarioInput = $("roleplayScenarioInput"), roleplayStarterSuggestions = $("roleplayStarterSuggestions");
const popupModal = $("popupModal"), popupCloseBtn = $("popupClose"), popupCancelBtn = $("popupCancel"), popupConfirmBtn = $("popupConfirm"); const popupModal = $("popupModal"), popupCloseBtn = $("popupClose"), popupCancelBtn = $("popupCancel"), popupConfirmBtn = $("popupConfirm");
const popupTitle = $("popupTitle"), popupEyebrow = $("popupEyebrow"), popupDescription = $("popupDescription"), popupField = $("popupField"); const popupTitle = $("popupTitle"), popupEyebrow = $("popupEyebrow"), popupDescription = $("popupDescription"), popupField = $("popupField");
const popupInputLabel = $("popupInputLabel"), popupInput = $("popupInput"); const popupInputLabel = $("popupInputLabel"), popupInput = $("popupInput");
const appNotice = $("appNotice"), quickPrompts = $("quickPrompts");
const authScreens = document.querySelectorAll(".auth-screen"), toggleButtons = document.querySelectorAll(".auth-toggle .toggle"); const authScreens = document.querySelectorAll(".auth-screen"), toggleButtons = document.querySelectorAll(".auth-toggle .toggle");
const themeNameTargets = document.querySelectorAll("[data-theme-name]"), themeLogoTargets = document.querySelectorAll("[data-theme-logo]"); const themeNameTargets = document.querySelectorAll("[data-theme-name]"), themeLogoTargets = document.querySelectorAll("[data-theme-logo]");
const requestedChatId = Number(new URLSearchParams(window.location.search).get("chat")) || null; const requestedChatId = Number(new URLSearchParams(window.location.search).get("chat")) || null;
@@ -35,6 +37,7 @@ let chatSessions = [], currentMessages = [], assistantPersonas = [], userPersona
let activeUserPersonaId = null, currentSummary = null, publishedPersonaIds = new Set(); let activeUserPersonaId = null, currentSummary = null, publishedPersonaIds = new Set();
let popupResolver = null, popupMode = null, popupLastFocus = null; let popupResolver = null, popupMode = null, popupLastFocus = null;
let roleplayPresetCharacterId = null; let roleplayPresetCharacterId = null;
let noticeTimer = null;
const themes = { const themes = {
"fakegpt": {name: "FakeGPT", short: "FG"}, "fakegpt": {name: "FakeGPT", short: "FG"},
@@ -64,7 +67,27 @@ const put = (url, data) => request(url, data, "PUT");
const del = (url) => request(url, null, "DELETE"); const del = (url) => request(url, null, "DELETE");
function setAuthMessage(message, state = "") { authMsg.textContent = message; authMsg.className = state ? `status ${state}` : "status"; } function setAuthMessage(message, state = "") { authMsg.textContent = message; authMsg.className = state ? `status ${state}` : "status"; }
function setNotice(message, state = "") { return {message, state}; } function setNotice(message = "", state = "") {
if (!appNotice) return {message, state};
if (noticeTimer) {
clearTimeout(noticeTimer);
noticeTimer = null;
}
if (!message) {
appNotice.textContent = "";
appNotice.className = "status inline-status hidden";
return {message, state};
}
appNotice.textContent = message;
appNotice.className = state ? `status inline-status ${state}` : "status inline-status";
noticeTimer = window.setTimeout(() => {
if (appNotice.textContent === message) {
appNotice.textContent = "";
appNotice.className = "status inline-status hidden";
}
}, state === "error" ? 6000 : 3200);
return {message, state};
}
function setPersonaFormNotice(message = "", state = "") { function setPersonaFormNotice(message = "", state = "") {
if (!personaFormNotice) return; if (!personaFormNotice) return;
if (!message) { if (!message) {
@@ -85,6 +108,82 @@ function setMessageContent(el, content) {
} }
} }
function getChatById(id) { return chatSessions.find((chat) => chat.id === id); } function getChatById(id) { return chatSessions.find((chat) => chat.id === id); }
function summarizeText(value, limit = 96) {
const normalized = String(value || "").replace(/\s+/g, " ").trim();
if (!normalized) return "";
return normalized.length > limit ? `${normalized.slice(0, limit - 3).trimEnd()}...` : normalized;
}
function getPersonaName(id, items) {
return items.find((persona) => persona.id === id)?.name || "";
}
function getRoleplayStarterSuggestions() {
const assistantPersonaId = Number(roleplayCharacterSelect?.value);
const userPersonaId = roleplayUserPersonaSelect?.value ? Number(roleplayUserPersonaSelect.value) : null;
const assistantName = getPersonaName(assistantPersonaId, assistantPersonas) || "the character";
const userName = userPersonaId ? getPersonaName(userPersonaId, userPersonas) : "me";
return [
`${assistantName} unexpectedly crosses paths with ${userName} in a place that matters to them both.`,
`${assistantName} already knows something important about ${userName} and confronts them with it.`,
`${assistantName} and ${userName} are forced to cooperate while something goes wrong around them.`,
`${assistantName} meets ${userName} in a quiet, intimate scene with unresolved tension.`,
`${assistantName} draws ${userName} into a high-stakes problem that starts immediately.`
];
}
function updateRoleplaySuggestions() {
if (!roleplayStarterSuggestions) return;
roleplayStarterSuggestions.innerHTML = "";
getRoleplayStarterSuggestions().forEach((suggestion) => {
const button = document.createElement("button");
button.type = "button";
button.className = "prompt-chip";
button.textContent = suggestion;
button.addEventListener("click", () => {
if (roleplayScenarioInput) {
roleplayScenarioInput.value = suggestion;
roleplayScenarioInput.focus();
}
});
roleplayStarterSuggestions.appendChild(button);
});
}
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() {
if (!quickPrompts) return;
const activeChat = getChatById(activeChatId);
const prompts = getQuickPromptsForChat(activeChat);
quickPrompts.innerHTML = "";
quickPrompts.classList.toggle("hidden", prompts.length === 0);
prompts.forEach((prompt) => {
const button = document.createElement("button");
button.type = "button";
button.className = "prompt-chip";
button.textContent = prompt;
button.addEventListener("click", () => {
msgInput.value = prompt;
msgInput.dispatchEvent(new Event("input"));
msgInput.focus();
});
quickPrompts.appendChild(button);
});
}
function updateComposerPlaceholder() {
const activeChat = getChatById(activeChatId);
msgInput.placeholder = activeChat?.assistant_persona_id
? "Reply in the scene..."
: "Type your message...";
}
function addMessage(content, isUser = false, isLoading = false) { function addMessage(content, isUser = false, isLoading = false) {
const msgDiv = document.createElement("div"); const msgDiv = document.createElement("div");
@@ -105,7 +204,8 @@ function addMessage(content, isUser = false, isLoading = false) {
} else { } else {
const header = document.createElement("div"), body = document.createElement("div"); const header = document.createElement("div"), body = document.createElement("div");
header.className = "msg-header"; body.className = "msg-content"; header.className = "msg-header"; body.className = "msg-content";
header.textContent = isUser ? "You" : "Assistant"; const activeChat = getChatById(activeChatId);
header.textContent = isUser ? "You" : (activeChat?.assistant_persona_name || "Assistant");
setMessageContent(body, content); setMessageContent(body, content);
msgDiv.append(header, body); msgDiv.append(header, body);
} }
@@ -119,6 +219,8 @@ function renderMessages() {
currentMessages.forEach((msg) => addMessage(msg.content, msg.role === "user")); currentMessages.forEach((msg) => addMessage(msg.content, msg.role === "user"));
updateChatParticipants(); updateChatParticipants();
updateChatActionState(); updateChatActionState();
renderQuickPrompts();
updateComposerPlaceholder();
} }
function updateWorkspaceCopy() { function updateWorkspaceCopy() {
@@ -130,6 +232,8 @@ function updateWorkspaceCopy() {
sessionUser.textContent = currentUsername || "-"; sessionUser.textContent = currentUsername || "-";
} }
updateChatParticipants(); updateChatParticipants();
renderQuickPrompts();
updateComposerPlaceholder();
} }
function setRoleplayStarterNotice(message = "", state = "") { function setRoleplayStarterNotice(message = "", state = "") {
@@ -145,11 +249,12 @@ function setRoleplayStarterNotice(message = "", state = "") {
function updateChatParticipants() { function updateChatParticipants() {
const chat = getChatById(activeChatId); const chat = getChatById(activeChatId);
if (!chatModePill || !chatCharacterPill || !chatUserPersonaPill) return; if (!chatModePill || !chatCharacterPill || !chatUserPersonaPill || !chatScenePill) return;
if (!chat) { if (!chat) {
chatModePill.textContent = "No chat selected"; chatModePill.textContent = "No chat selected";
chatCharacterPill.classList.add("hidden"); chatCharacterPill.classList.add("hidden");
chatUserPersonaPill.classList.add("hidden"); chatUserPersonaPill.classList.add("hidden");
chatScenePill.classList.add("hidden");
return; return;
} }
if (chat.assistant_persona_id) { if (chat.assistant_persona_id) {
@@ -163,11 +268,18 @@ function updateChatParticipants() {
chatUserPersonaPill.textContent = "You as: yourself"; chatUserPersonaPill.textContent = "You as: yourself";
chatUserPersonaPill.classList.remove("hidden"); chatUserPersonaPill.classList.remove("hidden");
} }
if (chat.scenario_summary) {
chatScenePill.textContent = `Scene: ${summarizeText(chat.scenario_summary, 112)}`;
chatScenePill.classList.remove("hidden");
} else {
chatScenePill.classList.add("hidden");
}
return; return;
} }
chatModePill.textContent = "Assistant mode"; chatModePill.textContent = "Assistant mode";
chatCharacterPill.classList.add("hidden"); chatCharacterPill.classList.add("hidden");
chatUserPersonaPill.classList.add("hidden"); chatUserPersonaPill.classList.add("hidden");
chatScenePill.classList.add("hidden");
} }
function updateContextRail() { function updateContextRail() {
@@ -292,8 +404,8 @@ function updateActivePersonaStatus(items, activeId, statusEl, emptyText, prefix)
function setActivePersonaStatus() { function setActivePersonaStatus() {
if (activePersonaStatus) { if (activePersonaStatus) {
activePersonaStatus.textContent = assistantPersonas.length activePersonaStatus.textContent = assistantPersonas.length
? "Pick a character, choose a user persona, and start the roleplay." ? "Choose an AI Character, pair it with a user persona if needed, and start the scene."
: "No characters yet. Create one to start a roleplay."; : "No AI Characters yet. Create one to start a roleplay.";
} }
updateActivePersonaStatus(userPersonas, activeUserPersonaId, activeUserPersonaStatus, "No default user persona set.", "Default user persona"); updateActivePersonaStatus(userPersonas, activeUserPersonaId, activeUserPersonaStatus, "No default user persona set.", "Default user persona");
if (clearUserPersonaBtn) clearUserPersonaBtn.disabled = !activeUserPersonaId; if (clearUserPersonaBtn) clearUserPersonaBtn.disabled = !activeUserPersonaId;
@@ -347,12 +459,14 @@ function populateRoleplayStarter() {
option.selected = persona.id === activeUserPersonaId; option.selected = persona.id === activeUserPersonaId;
roleplayUserPersonaSelect.appendChild(option); roleplayUserPersonaSelect.appendChild(option);
}); });
updateRoleplaySuggestions();
} }
function openRoleplayStarter(characterId = null) { function openRoleplayStarter(characterId = null) {
closePersonaPopover(); closePersonaPopover();
closeModelPopover(); closeModelPopover();
roleplayPresetCharacterId = characterId; roleplayPresetCharacterId = characterId;
populateRoleplayStarter(); populateRoleplayStarter();
if (roleplayScenarioInput) roleplayScenarioInput.value = "";
setRoleplayStarterNotice(""); setRoleplayStarterNotice("");
roleplayStarterModal.classList.remove("hidden"); roleplayStarterModal.classList.remove("hidden");
if (roleplayCharacterSelect.options.length) roleplayCharacterSelect.focus(); if (roleplayCharacterSelect.options.length) roleplayCharacterSelect.focus();
@@ -362,6 +476,7 @@ function closeRoleplayStarter() {
roleplayStarterModal.classList.add("hidden"); roleplayStarterModal.classList.add("hidden");
roleplayPresetCharacterId = null; roleplayPresetCharacterId = null;
roleplayStarterConfirm.disabled = false; roleplayStarterConfirm.disabled = false;
if (roleplayScenarioInput) roleplayScenarioInput.value = "";
setRoleplayStarterNotice(""); setRoleplayStarterNotice("");
} }
function closeModelPopover() { function closeModelPopover() {
@@ -539,7 +654,12 @@ async function unpublishPersona(id) { const res = await post(`/personas/${id}/un
function setLoadingState(loading) { function setLoadingState(loading) {
isProcessing = loading; sendBtn.disabled = loading; msgInput.disabled = loading; isProcessing = loading; sendBtn.disabled = loading; msgInput.disabled = loading;
sendBtn.classList.toggle("loading", loading); msgInput.placeholder = loading ? "Processing response..." : "Type your message..."; sendBtn.classList.toggle("loading", loading);
if (loading) {
msgInput.placeholder = "Processing response...";
} else {
updateComposerPlaceholder();
}
} }
function showAuthScreen(target) { function showAuthScreen(target) {
@@ -578,14 +698,16 @@ async function createNewChat() {
async function startRoleplay() { async function startRoleplay() {
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() || "";
if (!assistantPersonaId) { if (!assistantPersonaId) {
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;
setRoleplayStarterNotice("Starting roleplay..."); setRoleplayStarterNotice("Building the opening scene...");
const res = await post("/roleplays/start", { const res = await post("/roleplays/start", {
assistantPersonaId, assistantPersonaId,
userPersonaId, userPersonaId,
scenarioPrompt,
model: modelSelect.value model: modelSelect.value
}); });
roleplayStarterConfirm.disabled = false; roleplayStarterConfirm.disabled = false;
@@ -595,7 +717,7 @@ async function startRoleplay() {
closeRoleplayStarter(); closeRoleplayStarter();
await loadChatSessions(); await loadChatSessions();
await setActiveChat(res.chat.id); await setActiveChat(res.chat.id);
setNotice(res.generatedInitialMessage ? "Roleplay started." : "Roleplay reopened.", "success"); setNotice(res.generatedInitialMessage ? "Roleplay started with a fresh scene." : "Roleplay reopened.", "success");
} }
async function renameChat(id) { async function renameChat(id) {
@@ -778,6 +900,8 @@ personaModal.addEventListener("click", (event) => { if (event.target === persona
roleplayStarterClose.addEventListener("click", closeRoleplayStarter); roleplayStarterClose.addEventListener("click", closeRoleplayStarter);
roleplayStarterCancel.addEventListener("click", closeRoleplayStarter); roleplayStarterCancel.addEventListener("click", closeRoleplayStarter);
roleplayStarterConfirm.addEventListener("click", () => { void startRoleplay(); }); roleplayStarterConfirm.addEventListener("click", () => { void startRoleplay(); });
roleplayCharacterSelect.addEventListener("change", updateRoleplaySuggestions);
roleplayUserPersonaSelect.addEventListener("change", updateRoleplaySuggestions);
roleplayStarterModal.addEventListener("click", (event) => { if (event.target === roleplayStarterModal) closeRoleplayStarter(); }); roleplayStarterModal.addEventListener("click", (event) => { if (event.target === roleplayStarterModal) closeRoleplayStarter(); });
popupConfirmBtn.addEventListener("click", () => { popupConfirmBtn.addEventListener("click", () => {
if (popupMode === "prompt") { if (popupMode === "prompt") {
+11 -6
View File
@@ -25,6 +25,11 @@
<p id="settingsNotice" class="status">Loading settings...</p> <p id="settingsNotice" class="status">Loading settings...</p>
<section class="settings-quick-actions">
<button id="quickCreateAiCharacter" type="button" class="primary-action">Create AI Character</button>
<button id="quickCreateUserPersona" type="button" class="secondary-action">Create User Persona</button>
</section>
<section class="settings-layout"> <section class="settings-layout">
<aside class="settings-nav" aria-label="Settings sections"> <aside class="settings-nav" aria-label="Settings sections">
<button class="settings-nav-item active" type="button" data-settings-view="personal">Personal</button> <button class="settings-nav-item active" type="button" data-settings-view="personal">Personal</button>
@@ -88,11 +93,11 @@
<div class="settings-panel"> <div class="settings-panel">
<div class="settings-section-header"> <div class="settings-section-header">
<div> <div>
<p class="eyebrow">Manage</p> <p class="eyebrow">Create & manage</p>
<h2>My AI Characters</h2> <h2>My AI Characters</h2>
<p class="subtitle">Create, edit, publish, and delete your AI Characters.</p> <p class="subtitle">Build characters for roleplay chats and publish them to the market when they are ready.</p>
</div> </div>
<button id="createAiCharacter" type="button" class="secondary-action">Create persona</button> <button id="createAiCharacter" type="button" class="secondary-action">Create AI Character</button>
</div> </div>
<div id="settingsAiCharacterList" class="persona-list"></div> <div id="settingsAiCharacterList" class="persona-list"></div>
</div> </div>
@@ -102,11 +107,11 @@
<div class="settings-panel"> <div class="settings-panel">
<div class="settings-section-header"> <div class="settings-section-header">
<div> <div>
<p class="eyebrow">Manage</p> <p class="eyebrow">Create & manage</p>
<h2>My Personas</h2> <h2>My Personas</h2>
<p class="subtitle">Create and manage the user personas you can speak as in roleplay chats.</p> <p class="subtitle">Create the personas you want to speak as, then set one as default for faster roleplay setup.</p>
</div> </div>
<button id="createUserPersona" type="button" class="secondary-action">Create persona</button> <button id="createUserPersona" type="button" class="secondary-action">Create user persona</button>
</div> </div>
<div id="settingsUserPersonaList" class="persona-list"></div> <div id="settingsUserPersonaList" class="persona-list"></div>
</div> </div>
+25 -8
View File
@@ -13,6 +13,8 @@ const settingsNavItems = document.querySelectorAll("[data-settings-view]");
const settingsPanels = document.querySelectorAll("[data-settings-panel]"); const settingsPanels = document.querySelectorAll("[data-settings-panel]");
const createAiCharacterBtn = $("createAiCharacter"); const createAiCharacterBtn = $("createAiCharacter");
const createUserPersonaBtn = $("createUserPersona"); const createUserPersonaBtn = $("createUserPersona");
const quickCreateAiCharacterBtn = $("quickCreateAiCharacter");
const quickCreateUserPersonaBtn = $("quickCreateUserPersona");
const settingsAiCharacterList = $("settingsAiCharacterList"); const settingsAiCharacterList = $("settingsAiCharacterList");
const settingsUserPersonaList = $("settingsUserPersonaList"); const settingsUserPersonaList = $("settingsUserPersonaList");
const settingsPersonaModal = $("settingsPersonaModal"); const settingsPersonaModal = $("settingsPersonaModal");
@@ -101,8 +103,8 @@ function syncPersonaExamplesField() {
const isAssistant = settingsPersonaType.value === "assistant"; const isAssistant = settingsPersonaType.value === "assistant";
settingsPersonaExamplesField.classList.toggle("hidden", !isAssistant); settingsPersonaExamplesField.classList.toggle("hidden", !isAssistant);
settingsPersonaHelper.textContent = isAssistant settingsPersonaHelper.textContent = isAssistant
? "AI Characters define who the character is, how they speak, and what kind of scene they lead." ? "Create the character you want to roleplay with."
: "User personas define who you are when starting roleplay chats with AI Characters."; : "Create the persona you want to speak as in roleplay.";
$("settingsPersonaSave").textContent = isAssistant ? "Save AI Character" : "Save persona"; $("settingsPersonaSave").textContent = isAssistant ? "Save AI Character" : "Save persona";
} }
@@ -134,11 +136,17 @@ function closePersonaModal() {
function renderPersonaList(personas, listElement, type) { function renderPersonaList(personas, listElement, type) {
listElement.innerHTML = ""; listElement.innerHTML = "";
if (!personas.length) { if (!personas.length) {
const empty = document.createElement("p"); const empty = document.createElement("div");
empty.className = "status persona-status"; const message = document.createElement("p");
empty.textContent = type === "assistant" const button = document.createElement("button");
? "No AI Characters yet. Create one to get started." empty.className = "persona-empty-state";
: "No user personas yet. Create one to talk as someone specific."; message.className = "status persona-status";
message.textContent = type === "assistant" ? "No AI Characters yet." : "No user personas yet.";
button.type = "button";
button.className = "secondary-action";
button.textContent = type === "assistant" ? "Create AI Character" : "Create user persona";
button.addEventListener("click", () => openPersonaModal(null, type));
empty.append(message, button);
listElement.appendChild(empty); listElement.appendChild(empty);
return; return;
} }
@@ -213,6 +221,7 @@ async function loadPersonas() {
} }
async function savePersona() { async function savePersona() {
const wasEditing = Boolean(editingPersonaId);
const payload = { const payload = {
personaType: settingsPersonaType.value, personaType: settingsPersonaType.value,
name: settingsPersonaName.value.trim(), name: settingsPersonaName.value.trim(),
@@ -240,7 +249,7 @@ async function savePersona() {
await loadPersonas(); await loadPersonas();
closePersonaModal(); closePersonaModal();
setSettingsView(payload.personaType === "assistant" ? "my-ai-characters" : "my-personas"); setSettingsView(payload.personaType === "assistant" ? "my-ai-characters" : "my-personas");
setNotice(editingPersonaId ? "Persona updated." : "Persona created.", "success"); setNotice(wasEditing ? "Persona updated." : "Persona created.", "success");
} }
async function deletePersona(id) { async function deletePersona(id) {
@@ -322,6 +331,14 @@ settingsThemeSelect.addEventListener("change", (event) => {
}); });
settingsNavItems.forEach((item) => item.addEventListener("click", () => setSettingsView(item.dataset.settingsView))); settingsNavItems.forEach((item) => item.addEventListener("click", () => setSettingsView(item.dataset.settingsView)));
quickCreateAiCharacterBtn.addEventListener("click", () => {
setSettingsView("my-ai-characters");
openPersonaModal(null, "assistant");
});
quickCreateUserPersonaBtn.addEventListener("click", () => {
setSettingsView("my-personas");
openPersonaModal(null, "user");
});
createAiCharacterBtn.addEventListener("click", () => openPersonaModal(null, "assistant")); createAiCharacterBtn.addEventListener("click", () => openPersonaModal(null, "assistant"));
createUserPersonaBtn.addEventListener("click", () => openPersonaModal(null, "user")); createUserPersonaBtn.addEventListener("click", () => openPersonaModal(null, "user"));
settingsPersonaClose.addEventListener("click", closePersonaModal); settingsPersonaClose.addEventListener("click", closePersonaModal);
+27
View File
@@ -530,6 +530,7 @@ select:focus {
.inline-status { .inline-status {
min-height: auto; min-height: auto;
padding: 10px 12px; padding: 10px 12px;
flex: 1 1 260px;
} }
.consent-container { .consent-container {
@@ -1043,6 +1044,9 @@ select:focus {
} }
.prompt-chip { .prompt-chip {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 9px 12px; padding: 9px 12px;
border-radius: 999px; border-radius: 999px;
background: rgba(255, 251, 245, 0.65); background: rgba(255, 251, 245, 0.65);
@@ -1453,6 +1457,8 @@ select:focus {
border-radius: 16px; border-radius: 16px;
background: rgba(24, 32, 31, 0.04); background: rgba(24, 32, 31, 0.04);
border: 1px solid rgba(24, 32, 31, 0.08); border: 1px solid rgba(24, 32, 31, 0.08);
display: grid;
gap: 14px;
} }
.persona-form > .status { .persona-form > .status {
@@ -1594,6 +1600,17 @@ select:focus {
gap: 16px; gap: 16px;
} }
.settings-quick-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.settings-quick-actions .primary-action,
.settings-quick-actions .secondary-action {
min-width: 210px;
}
.settings-shell .market-header-simple { .settings-shell .market-header-simple {
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr) auto; grid-template-columns: minmax(0, 1fr) auto;
@@ -1710,6 +1727,16 @@ select:focus {
color: var(--text-muted); color: var(--text-muted);
} }
.persona-empty-state {
display: grid;
justify-items: start;
gap: 10px;
padding: 16px;
border-radius: 14px;
background: rgba(24, 32, 31, 0.04);
border: 1px dashed rgba(24, 32, 31, 0.14);
}
body[data-theme="fakegpt"] { body[data-theme="fakegpt"] {
--bg: #0b0f0d; --bg: #0b0f0d;
--bg-soft: #111715; --bg-soft: #111715;
+139 -15
View File
@@ -135,6 +135,10 @@ db.prepare(`
INTEGER, INTEGER,
user_persona_id user_persona_id
INTEGER, INTEGER,
scenario_prompt
TEXT,
scenario_summary
TEXT,
created_at created_at
DATETIME DATETIME
DEFAULT DEFAULT
@@ -331,7 +335,7 @@ const getLegacyChatsStmt = db.prepare(`
ORDER BY datetime(created_at) ORDER BY datetime(created_at)
`); `);
const insertChatSessionStmt = db.prepare( const insertChatSessionStmt = db.prepare(
"INSERT INTO chat_sessions (username, title, assistant_persona_id, user_persona_id) VALUES (?, ?, ?, ?)" "INSERT INTO chat_sessions (username, title, assistant_persona_id, user_persona_id, scenario_prompt, scenario_summary) VALUES (?, ?, ?, ?, ?, ?)"
); );
const listChatSessionsStmt = db.prepare(` const listChatSessionsStmt = db.prepare(`
SELECT cs.id, SELECT cs.id,
@@ -344,6 +348,8 @@ const listChatSessionsStmt = db.prepare(`
p.name AS assistant_persona_name, p.name AS assistant_persona_name,
cs.user_persona_id, cs.user_persona_id,
up.name AS user_persona_name, up.name AS user_persona_name,
cs.scenario_prompt,
cs.scenario_summary,
cs.created_at, cs.created_at,
cs.updated_at cs.updated_at
FROM chat_sessions cs FROM chat_sessions cs
@@ -369,7 +375,9 @@ const getChatSessionStmt = db.prepare(`
cs.assistant_persona_id, cs.assistant_persona_id,
p.name AS assistant_persona_name, p.name AS assistant_persona_name,
cs.user_persona_id, cs.user_persona_id,
up.name AS user_persona_name up.name AS user_persona_name,
cs.scenario_prompt,
cs.scenario_summary
FROM chat_sessions cs FROM chat_sessions cs
LEFT JOIN personas p ON p.id = cs.assistant_persona_id LEFT JOIN personas p ON p.id = cs.assistant_persona_id
LEFT JOIN personas up ON up.id = cs.user_persona_id LEFT JOIN personas up ON up.id = cs.user_persona_id
@@ -387,7 +395,9 @@ const getChatSessionByParticipantsStmt = db.prepare(`
cs.assistant_persona_id, cs.assistant_persona_id,
p.name AS assistant_persona_name, p.name AS assistant_persona_name,
cs.user_persona_id, cs.user_persona_id,
up.name AS user_persona_name up.name AS user_persona_name,
cs.scenario_prompt,
cs.scenario_summary
FROM chat_sessions cs FROM chat_sessions cs
LEFT JOIN personas p ON p.id = cs.assistant_persona_id LEFT JOIN personas p ON p.id = cs.assistant_persona_id
LEFT JOIN personas up ON up.id = cs.user_persona_id LEFT JOIN personas up ON up.id = cs.user_persona_id
@@ -405,6 +415,10 @@ const updateChatSessionTitleStmt = db.prepare(
"UPDATE chat_sessions SET title = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND username = ?" "UPDATE chat_sessions SET title = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND username = ?"
); );
const updateChatSessionSceneStmt = db.prepare(
"UPDATE chat_sessions SET scenario_prompt = ?, scenario_summary = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND username = ?"
);
const touchChatSessionStmt = db.prepare( const touchChatSessionStmt = db.prepare(
"UPDATE chat_sessions SET updated_at = CURRENT_TIMESTAMP WHERE id = ? AND username = ?" "UPDATE chat_sessions SET updated_at = CURRENT_TIMESTAMP WHERE id = ? AND username = ?"
); );
@@ -793,6 +807,14 @@ const hasChatUserPersonaLink = chatSessionColumns.some(column => column.name ===
if (!hasChatUserPersonaLink) { if (!hasChatUserPersonaLink) {
db.prepare("ALTER TABLE chat_sessions ADD COLUMN user_persona_id INTEGER").run(); db.prepare("ALTER TABLE chat_sessions ADD COLUMN user_persona_id INTEGER").run();
} }
const hasScenarioPrompt = chatSessionColumns.some(column => column.name === "scenario_prompt");
if (!hasScenarioPrompt) {
db.prepare("ALTER TABLE chat_sessions ADD COLUMN scenario_prompt TEXT").run();
}
const hasScenarioSummary = chatSessionColumns.some(column => column.name === "scenario_summary");
if (!hasScenarioSummary) {
db.prepare("ALTER TABLE chat_sessions ADD COLUMN scenario_summary TEXT").run();
}
const FileStore = FileStoreFactory(session); const FileStore = FileStoreFactory(session);
app.use(bodyParser.json()); app.use(bodyParser.json());
@@ -1010,19 +1032,95 @@ const buildUserPersonaPrompt = (persona) => {
return lines.join("\n"); return lines.join("\n");
}; };
const createPersonaChat = (user, persona, userPersonaId = null) => { const summarizePersona = (persona) => {
const chatId = insertChatSessionStmt.run(user, persona.name, persona.id, userPersonaId).lastInsertRowid; if (!persona) return "";
return [
persona.name ? `Name: ${persona.name}` : null,
persona.pronouns ? `Pronouns: ${persona.pronouns}` : null,
persona.background ? `Background: ${persona.background}` : null,
persona.details ? `Traits: ${persona.details}` : null
].filter(Boolean).join(" | ");
};
const clampText = (value, limit = 260) => {
const normalized = String(value || "").replace(/\s+/g, " ").trim();
if (normalized.length <= limit) return normalized;
return `${normalized.slice(0, limit - 3).trimEnd()}...`;
};
const buildRoleplaySceneSummary = (assistantPersona, userPersona, scenarioPrompt = "") => {
const assistantName = assistantPersona?.name || "The assistant";
const userName = userPersona?.name || "the user";
const assistantHook = assistantPersona?.details || assistantPersona?.background || "stays strongly in character";
const userHook = userPersona?.details || userPersona?.background || "enters the scene as themselves";
const scenarioSeed = scenarioPrompt
? clampText(scenarioPrompt, 180)
: `A fitting opening grows naturally from ${assistantName}'s role, goals, and manner toward ${userName}.`;
return clampText(
`${assistantName} leads the opening. Their approach is shaped by ${assistantHook}. ` +
`${userName} is framed through ${userHook}. ` +
`Scene seed: ${scenarioSeed}`,
340
);
};
const buildRoleplayDirectionPrompt = ({assistantPersona, userPersona, scenarioPrompt, sceneSummary}) => {
const lines = [
"ROLEPLAY DIRECTION:",
"Create an immersive one-on-one roleplay between the assistant persona and the user.",
"Ground the interaction in a specific scenario, place, and immediate dramatic situation.",
"Make the assistant's first instinct, language, and priorities fit their persona exactly.",
"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.",
"Prefer a concrete opening beat over vague exposition.",
"Include a subtle hook, tension, invitation, or problem that gives the user something to respond to.",
"",
`SCENE SUMMARY: ${sceneSummary}`
];
if (scenarioPrompt) {
lines.push(`USER SCENARIO SEED: ${scenarioPrompt}`);
}
if (assistantPersona) {
lines.push("", `ASSISTANT SNAPSHOT: ${summarizePersona(assistantPersona)}`);
}
if (userPersona) {
lines.push(`USER SNAPSHOT: ${summarizePersona(userPersona)}`);
}
return lines.join("\n");
};
const buildRoleplayOpenerPrompt = ({assistantPersona, userPersona, scenarioPrompt, sceneSummary}) => {
const assistantName = assistantPersona?.name || "the assistant";
const userName = userPersona?.name || "the user";
const sceneSeed = scenarioPrompt
? `Use this scenario seed: ${scenarioPrompt}`
: `Invent a fresh scenario that suits ${assistantName} and how they would realistically meet or confront ${userName}.`;
return [
`Write the first in-character message from ${assistantName}.`,
sceneSeed,
`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.",
"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 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); return getChatSessionStmt.get(chatId, user);
}; };
const getOrCreatePersonaChat = (user, persona, userPersonaId = null) => { const getOrCreatePersonaChat = (user, persona, userPersonaId = null, scenarioPrompt = null, scenarioSummary = null) => {
const existing = getChatSessionByParticipantsStmt.get(user, persona.id, userPersonaId, userPersonaId); const existing = getChatSessionByParticipantsStmt.get(user, persona.id, userPersonaId, userPersonaId);
if (existing) return existing; if (existing) return existing;
const sessionCount = countChatSessionsStmt.get(user).count; const sessionCount = countChatSessionsStmt.get(user).count;
if (sessionCount >= 10) { if (sessionCount >= 10) {
throw new Error("CHAT_LIMIT_REACHED"); throw new Error("CHAT_LIMIT_REACHED");
} }
return createPersonaChat(user, persona, userPersonaId); return createPersonaChat(user, persona, userPersonaId, scenarioPrompt, scenarioSummary);
}; };
const ensureModelState = (model) => { const ensureModelState = (model) => {
@@ -1132,7 +1230,7 @@ app.post("/chats", requireLogin, (req, res) => {
} }
const title = (req.body?.title || "New chat").trim() || "New chat"; const title = (req.body?.title || "New chat").trim() || "New chat";
const chatId = insertChatSessionStmt.run(user, title, null, null).lastInsertRowid; const chatId = insertChatSessionStmt.run(user, title, null, null, null, null).lastInsertRowid;
const chat = getChatSessionStmt.get(chatId, user); const chat = getChatSessionStmt.get(chatId, user);
res.json({chat, existing: false}); res.json({chat, existing: false});
}); });
@@ -1281,7 +1379,7 @@ app.post("/personas/:id/chat", requireLogin, (req, res) => {
} }
let chat; let chat;
try { try {
chat = getOrCreatePersonaChat(user, persona, userPersonaId); chat = getOrCreatePersonaChat(user, persona, userPersonaId, null, null);
} catch (error) { } catch (error) {
if (error.message === "CHAT_LIMIT_REACHED") { if (error.message === "CHAT_LIMIT_REACHED") {
return res.status(400).json({error: "Maximum of 10 chats reached. Please delete an old chat to create a new one."}); return res.status(400).json({error: "Maximum of 10 chats reached. Please delete an old chat to create a new one."});
@@ -1450,9 +1548,11 @@ app.post("/personas/market/:id/chat", requireLogin, (req, res) => {
} }
const persona = getPersonaStmt.get(personaId, user); const persona = getPersonaStmt.get(personaId, user);
const scenarioPrompt = clampText(req.body?.scenarioPrompt || "", 240) || null;
const sceneSummary = buildRoleplaySceneSummary(persona, userPersona, scenarioPrompt);
let chat; let chat;
try { try {
chat = getOrCreatePersonaChat(user, persona, userPersonaId); chat = getOrCreatePersonaChat(user, persona, userPersonaId, scenarioPrompt, sceneSummary);
} catch (error) { } catch (error) {
if (error.message === "CHAT_LIMIT_REACHED") { if (error.message === "CHAT_LIMIT_REACHED") {
return res.status(400).json({error: "Maximum of 10 chats reached. Please delete an old chat to create a new one."}); return res.status(400).json({error: "Maximum of 10 chats reached. Please delete an old chat to create a new one."});
@@ -1463,14 +1563,17 @@ app.post("/personas/market/:id/chat", requireLogin, (req, res) => {
if (existingMessages.length > 0) { if (existingMessages.length > 0) {
return res.json({persona, chat, existing: true, generatedInitialMessage: false}); return res.json({persona, chat, existing: true, generatedInitialMessage: false});
} }
updateChatSessionSceneStmt.run(scenarioPrompt, sceneSummary, chat.id, user);
chat = getChatSessionStmt.get(chat.id, user);
Promise.resolve(generateModelReply("mistral:latest", [ Promise.resolve(generateModelReply("mistral:latest", [
{role: "system", content: buildPersonaPrompt(persona)}, {role: "system", content: buildPersonaPrompt(persona)},
...(userPersona ? [{role: "system", content: buildUserPersonaPrompt(userPersona)}] : []), ...(userPersona ? [{role: "system", content: buildUserPersonaPrompt(userPersona)}] : []),
{role: "user", content: "Open this roleplay with the very first in-character message. Set the scene naturally, stay in persona, and do not ask for permission to begin."} {role: "system", content: buildRoleplayDirectionPrompt({assistantPersona: persona, userPersona, scenarioPrompt, sceneSummary})},
{role: "user", content: buildRoleplayOpenerPrompt({assistantPersona: persona, userPersona, scenarioPrompt, sceneSummary})}
])).then((opener) => { ])).then((opener) => {
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, existing: false, generatedInitialMessage: true, opener}); res.json({persona, chat: getChatSessionStmt.get(chat.id, user), existing: false, generatedInitialMessage: true, opener});
}).catch(() => { }).catch(() => {
res.json({persona, chat, existing: false, generatedInitialMessage: false}); res.json({persona, chat, existing: false, generatedInitialMessage: false});
}); });
@@ -1481,6 +1584,7 @@ app.post("/roleplays/start", requireLogin, async (req, res) => {
const selectedModel = req.body?.model || "mistral:latest"; const selectedModel = req.body?.model || "mistral:latest";
const assistantPersonaId = Number(req.body?.assistantPersonaId); const assistantPersonaId = Number(req.body?.assistantPersonaId);
const userPersonaId = req.body?.userPersonaId ? Number(req.body.userPersonaId) : null; const userPersonaId = req.body?.userPersonaId ? Number(req.body.userPersonaId) : null;
const scenarioPrompt = clampText(req.body?.scenarioPrompt || "", 240) || null;
if (!assistantPersonaId) { if (!assistantPersonaId) {
return res.status(400).json({error: "assistantPersonaId is required"}); return res.status(400).json({error: "assistantPersonaId is required"});
@@ -1500,8 +1604,9 @@ app.post("/roleplays/start", requireLogin, async (req, res) => {
} }
let chat; let chat;
const sceneSummary = buildRoleplaySceneSummary(assistantPersona, userPersona, scenarioPrompt);
try { try {
chat = getOrCreatePersonaChat(user, assistantPersona, userPersonaId); chat = getOrCreatePersonaChat(user, assistantPersona, userPersonaId, scenarioPrompt, sceneSummary);
} catch (error) { } catch (error) {
if (error.message === "CHAT_LIMIT_REACHED") { if (error.message === "CHAT_LIMIT_REACHED") {
return res.status(400).json({error: "Maximum of 10 chats reached. Please delete an old chat to create a new one."}); return res.status(400).json({error: "Maximum of 10 chats reached. Please delete an old chat to create a new one."});
@@ -1513,6 +1618,8 @@ app.post("/roleplays/start", requireLogin, async (req, res) => {
if (existingMessages.length > 0) { if (existingMessages.length > 0) {
return res.json({chat, existing: true, generatedInitialMessage: false}); return res.json({chat, existing: true, generatedInitialMessage: false});
} }
updateChatSessionSceneStmt.run(scenarioPrompt, sceneSummary, chat.id, user);
chat = getChatSessionStmt.get(chat.id, user);
const messagesPayload = [ const messagesPayload = [
{role: "system", content: buildPersonaPrompt(assistantPersona)} {role: "system", content: buildPersonaPrompt(assistantPersona)}
@@ -1520,9 +1627,13 @@ app.post("/roleplays/start", requireLogin, async (req, res) => {
if (userPersona) { if (userPersona) {
messagesPayload.push({role: "system", content: buildUserPersonaPrompt(userPersona)}); messagesPayload.push({role: "system", content: buildUserPersonaPrompt(userPersona)});
} }
messagesPayload.push({
role: "system",
content: buildRoleplayDirectionPrompt({assistantPersona, userPersona, scenarioPrompt, sceneSummary})
});
messagesPayload.push({ messagesPayload.push({
role: "user", role: "user",
content: "Open this roleplay with the very first in-character message. Set the scene naturally, stay in persona, and do not ask for permission to begin." content: buildRoleplayOpenerPrompt({assistantPersona, userPersona, scenarioPrompt, sceneSummary})
}); });
try { try {
@@ -1532,7 +1643,7 @@ app.post("/roleplays/start", requireLogin, async (req, res) => {
} }
insertChatMessageStmt.run(chat.id, "bot", opener); insertChatMessageStmt.run(chat.id, "bot", opener);
touchChatSessionStmt.run(chat.id, user); touchChatSessionStmt.run(chat.id, user);
return res.json({chat, existing: false, generatedInitialMessage: true, opener}); return res.json({chat: getChatSessionStmt.get(chat.id, user), existing: false, generatedInitialMessage: true, opener});
} catch { } catch {
return res.status(500).json({error: "Failed to generate the character opener"}); return res.status(500).json({error: "Failed to generate the character opener"});
} }
@@ -1556,6 +1667,8 @@ app.post("/chat", requireLogin, async (req, res) => {
try { try {
const activePersona = getAssistantPersonaForChatStmt.get(targetChatId, user); const activePersona = getAssistantPersonaForChatStmt.get(targetChatId, user);
const activeUserPersona = getUserPersonaForChatStmt.get(targetChatId, user) || getActiveUserPersonaStmt.get(user); const activeUserPersona = getUserPersonaForChatStmt.get(targetChatId, user) || getActiveUserPersonaStmt.get(user);
const sceneSummary = session.scenario_summary;
const scenarioPrompt = session.scenario_prompt;
const history = getRecentChatMessagesStmt.all(targetChatId); const history = getRecentChatMessagesStmt.all(targetChatId);
const conversation = [...history].reverse(); const conversation = [...history].reverse();
conversation.push({role: "user", content: message}); conversation.push({role: "user", content: message});
@@ -1574,6 +1687,17 @@ app.post("/chat", requireLogin, async (req, res) => {
content: buildUserPersonaPrompt(activeUserPersona) content: buildUserPersonaPrompt(activeUserPersona)
}); });
} }
if (activePersona && sceneSummary) {
messagesPayload.push({
role: "system",
content: buildRoleplayDirectionPrompt({
assistantPersona: activePersona,
userPersona: activeUserPersona,
scenarioPrompt,
sceneSummary
})
});
}
messagesPayload.push( messagesPayload.push(
...conversation.map(m => ({ ...conversation.map(m => ({
role: m.role === "bot" ? "assistant" : "user", role: m.role === "bot" ? "assistant" : "user",