try to make app more intuitive
This commit is contained in:
+9
-1
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user