Revamp overall theme
This commit is contained in:
+49
-23
@@ -80,9 +80,11 @@
|
|||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Chats</p>
|
<p class="eyebrow">Chats</p>
|
||||||
<h2>Saved threads</h2>
|
<h2>Stories & chats</h2>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-header-actions">
|
||||||
|
<button id="newChat" class="secondary-action" type="button">New chat</button>
|
||||||
</div>
|
</div>
|
||||||
<button id="newChat" class="secondary-action" type="button">New</button>
|
|
||||||
</div>
|
</div>
|
||||||
<label class="field compact-field">
|
<label class="field compact-field">
|
||||||
<span>Search</span>
|
<span>Search</span>
|
||||||
@@ -107,6 +109,11 @@
|
|||||||
<div class="chat-title">
|
<div class="chat-title">
|
||||||
<p class="eyebrow">Conversation</p>
|
<p class="eyebrow">Conversation</p>
|
||||||
<h2 id="activeChatTitle">New chat</h2>
|
<h2 id="activeChatTitle">New chat</h2>
|
||||||
|
<div id="chatParticipants" class="chat-participants">
|
||||||
|
<span id="chatModePill" class="context-pill chat-mode-pill">Assistant mode</span>
|
||||||
|
<span id="chatCharacterPill" class="context-pill hidden">Character: none</span>
|
||||||
|
<span id="chatUserPersonaPill" class="context-pill hidden">User persona: none</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-toolbar">
|
<div class="chat-toolbar">
|
||||||
<button id="renameChat" class="icon-button" type="button">Rename</button>
|
<button id="renameChat" class="icon-button" type="button">Rename</button>
|
||||||
@@ -144,24 +151,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="persona-menu">
|
<div class="persona-menu">
|
||||||
<button id="personaMenuButton" class="secondary-action" type="button" aria-expanded="false" aria-controls="personaPopover">
|
<button id="personaMenuButton" class="secondary-action" type="button" aria-expanded="false" aria-controls="personaPopover">
|
||||||
Personas
|
You
|
||||||
</button>
|
</button>
|
||||||
<div id="personaPopover" class="persona-popover hidden" role="dialog">
|
<div id="personaPopover" class="persona-popover hidden" role="dialog">
|
||||||
<div class="persona-section">
|
|
||||||
<div class="persona-section-header">
|
|
||||||
<div>
|
|
||||||
<p class="eyebrow">Assistant</p>
|
|
||||||
<h2>AI persona</h2>
|
|
||||||
<p class="persona-guidance">Changes the assistant's tone and character.</p>
|
|
||||||
</div>
|
|
||||||
<div class="persona-section-actions">
|
|
||||||
<button id="newPersona" class="secondary-action" type="button">New AI</button>
|
|
||||||
<button id="clearPersona" class="secondary-action" type="button">Clear</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p id="activePersonaStatus" class="status persona-status">No AI persona equipped.</p>
|
|
||||||
<div id="personaList" class="persona-list"></div>
|
|
||||||
</div>
|
|
||||||
<div class="persona-section">
|
<div class="persona-section">
|
||||||
<div class="persona-section-header">
|
<div class="persona-section-header">
|
||||||
<div>
|
<div>
|
||||||
@@ -170,11 +162,11 @@
|
|||||||
<p class="persona-guidance">Adds context for how the assistant should read you.</p>
|
<p class="persona-guidance">Adds context for how the assistant should read you.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="persona-section-actions">
|
<div class="persona-section-actions">
|
||||||
<button id="newUserPersona" class="secondary-action" type="button">New you</button>
|
|
||||||
<button id="clearUserPersona" class="secondary-action" type="button">Clear</button>
|
<button id="clearUserPersona" class="secondary-action" type="button">Clear</button>
|
||||||
|
<a href="/settings" class="secondary-action">Manage</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p id="activeUserPersonaStatus" class="status persona-status">No user persona set.</p>
|
<p id="activeUserPersonaStatus" class="status persona-status">No default user persona set.</p>
|
||||||
<div id="userPersonaList" class="persona-list"></div>
|
<div id="userPersonaList" class="persona-list"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -205,18 +197,18 @@
|
|||||||
<p class="eyebrow">Persona editor</p>
|
<p class="eyebrow">Persona editor</p>
|
||||||
<h3 id="personaFormTitle">Create persona</h3>
|
<h3 id="personaFormTitle">Create persona</h3>
|
||||||
</div>
|
</div>
|
||||||
<button id="personaClose" type="button" class="icon-button">Cancel</button>
|
<button id="personaClose" type="button" class="icon-button">Close</button>
|
||||||
</div>
|
</div>
|
||||||
<form id="personaForm" class="persona-form">
|
<form id="personaForm" class="persona-form">
|
||||||
<div class="persona-form-intro">
|
<div class="persona-form-intro">
|
||||||
<p class="persona-helper">AI personas shape the assistant. User personas add context for how replies should read you.</p>
|
<p class="persona-helper">AI Characters shape who you talk to. User personas add context for who you are in the scene.</p>
|
||||||
</div>
|
</div>
|
||||||
<p id="personaFormNotice" class="status hidden"></p>
|
<p id="personaFormNotice" class="status hidden"></p>
|
||||||
<div class="persona-form-grid">
|
<div class="persona-form-grid">
|
||||||
<label class="field field-wide">
|
<label class="field field-wide">
|
||||||
<span>Persona for</span>
|
<span>Persona for</span>
|
||||||
<select id="personaType">
|
<select id="personaType">
|
||||||
<option value="assistant">AI persona</option>
|
<option value="assistant">AI Character</option>
|
||||||
<option value="user">Your persona</option>
|
<option value="user">Your persona</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
@@ -258,6 +250,40 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="roleplayStarterModal" class="modal-overlay hidden">
|
||||||
|
<div class="modal-card roleplay-starter-card">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="persona-modal-heading">
|
||||||
|
<p class="eyebrow">Start roleplay</p>
|
||||||
|
<h3>Choose your scene</h3>
|
||||||
|
</div>
|
||||||
|
<button id="roleplayStarterClose" type="button" class="icon-button">Close</button>
|
||||||
|
</div>
|
||||||
|
<div class="roleplay-starter-intro">
|
||||||
|
<p class="persona-helper">Pick a character, optionally choose who you are in the scene, and the character will open the conversation with the first message.</p>
|
||||||
|
</div>
|
||||||
|
<p id="roleplayStarterNotice" class="status hidden"></p>
|
||||||
|
<div class="roleplay-starter-actions">
|
||||||
|
<button id="roleplayNewPersona" class="secondary-action" type="button">New persona</button>
|
||||||
|
<a href="/settings" class="secondary-action">Settings</a>
|
||||||
|
</div>
|
||||||
|
<div class="persona-form-grid">
|
||||||
|
<label class="field field-wide">
|
||||||
|
<span>Character</span>
|
||||||
|
<select id="roleplayCharacterSelect"></select>
|
||||||
|
</label>
|
||||||
|
<label class="field field-wide">
|
||||||
|
<span>User persona</span>
|
||||||
|
<select id="roleplayUserPersonaSelect"></select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="popup-actions">
|
||||||
|
<button id="roleplayStarterCancel" type="button" class="secondary-action">Cancel</button>
|
||||||
|
<button id="roleplayStarterConfirm" type="button">Start roleplay</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="popupModal" class="modal-overlay hidden">
|
<div id="popupModal" class="modal-overlay hidden">
|
||||||
<div class="modal-card popup-card" role="dialog" aria-modal="true" aria-labelledby="popupTitle" aria-describedby="popupDescription">
|
<div class="modal-card popup-card" role="dialog" aria-modal="true" aria-labelledby="popupTitle" aria-describedby="popupDescription">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
|
|||||||
+110
-11
@@ -15,7 +15,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Shared personas</p>
|
<p class="eyebrow">Shared personas</p>
|
||||||
<h1><span data-theme-name>FakeGPT</span> Market</h1>
|
<h1><span data-theme-name>FakeGPT</span> Market</h1>
|
||||||
<p class="subtitle">A plain list of personas other people decided to share.</p>
|
<p class="subtitle">Browse shared AI Characters and User Personas, or manage your own AI Characters.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="market-header-actions">
|
<div class="market-header-actions">
|
||||||
<button id="refreshMarket" class="secondary-action" type="button">Refresh</button>
|
<button id="refreshMarket" class="secondary-action" type="button">Refresh</button>
|
||||||
@@ -31,16 +31,55 @@
|
|||||||
|
|
||||||
<p id="marketStatus" class="status persona-status">Loading market personas...</p>
|
<p id="marketStatus" class="status persona-status">Loading market personas...</p>
|
||||||
|
|
||||||
<div class="persona-market-columns market-page-columns">
|
<section class="settings-layout market-settings-layout">
|
||||||
<div class="persona-market-column">
|
<aside class="settings-nav" aria-label="Market sections">
|
||||||
<h3>AI personas</h3>
|
<button class="settings-nav-item active" type="button" data-market-view="ai-characters">AI Characters</button>
|
||||||
<div id="marketPersonaList" class="persona-list"></div>
|
<button class="settings-nav-item" type="button" data-market-view="user-personas">User Personas</button>
|
||||||
|
<button class="settings-nav-item" type="button" data-market-view="my-personas">My Personas</button>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div class="settings-content">
|
||||||
|
<section class="settings-view active" data-market-panel="ai-characters">
|
||||||
|
<div class="settings-panel">
|
||||||
|
<div class="settings-section-header">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Discover</p>
|
||||||
|
<h2>AI Characters</h2>
|
||||||
|
<p class="subtitle">Find shared characters to start roleplay chats with.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="marketPersonaList" class="persona-list"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="settings-view" data-market-panel="user-personas">
|
||||||
|
<div class="settings-panel">
|
||||||
|
<div class="settings-section-header">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Discover</p>
|
||||||
|
<h2>User Personas</h2>
|
||||||
|
<p class="subtitle">Browse shared user personas you can collect for roleplay context.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="marketUserPersonaList" class="persona-list"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="settings-view" data-market-panel="my-personas">
|
||||||
|
<div class="settings-panel">
|
||||||
|
<div class="settings-section-header">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Manage</p>
|
||||||
|
<h2>My Personas</h2>
|
||||||
|
<p class="subtitle">Create and manage your own AI Characters.</p>
|
||||||
|
</div>
|
||||||
|
<button id="marketCreatePersona" type="button" class="secondary-action">Create persona</button>
|
||||||
|
</div>
|
||||||
|
<div id="marketOwnedPersonaList" class="persona-list"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<div class="persona-market-column">
|
</section>
|
||||||
<h3>User personas</h3>
|
|
||||||
<div id="marketUserPersonaList" class="persona-list"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="marketPreviewModal" class="modal-overlay hidden">
|
<div id="marketPreviewModal" class="modal-overlay hidden">
|
||||||
@@ -71,14 +110,74 @@
|
|||||||
<h4>Details</h4>
|
<h4>Details</h4>
|
||||||
<p id="marketPreviewDetails"></p>
|
<p id="marketPreviewDetails"></p>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4>Example dialogues</h4>
|
||||||
|
<p id="marketPreviewExampleDialogues"></p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="market-preview-actions">
|
<div class="market-preview-actions">
|
||||||
|
<label id="marketPreviewUserPersonaField" class="field market-preview-user-field hidden">
|
||||||
|
<span>Talk as</span>
|
||||||
|
<select id="marketPreviewUserPersonaSelect"></select>
|
||||||
|
</label>
|
||||||
<button id="marketPreviewConfirm" type="button" class="primary-action">Take persona</button>
|
<button id="marketPreviewConfirm" type="button" class="primary-action">Take persona</button>
|
||||||
<button id="marketPreviewCancel" type="button" class="secondary-action">Cancel</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="marketPersonaModal" class="modal-overlay hidden">
|
||||||
|
<div class="modal-card persona-modal-card">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="persona-modal-heading">
|
||||||
|
<p class="eyebrow">AI Character editor</p>
|
||||||
|
<h3 id="marketPersonaFormTitle">Create AI Character</h3>
|
||||||
|
</div>
|
||||||
|
<button id="marketPersonaClose" type="button" class="icon-button">Close</button>
|
||||||
|
</div>
|
||||||
|
<form id="marketPersonaForm" class="persona-form">
|
||||||
|
<div class="persona-form-intro">
|
||||||
|
<p class="persona-helper">Create or update your AI Characters for roleplay chats and marketplace publishing.</p>
|
||||||
|
</div>
|
||||||
|
<p id="marketPersonaFormNotice" class="status hidden"></p>
|
||||||
|
<div class="persona-form-grid">
|
||||||
|
<label class="field">
|
||||||
|
<span>Character name</span>
|
||||||
|
<input id="marketPersonaName" placeholder="Character name" required/>
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>Pronouns</span>
|
||||||
|
<input id="marketPersonaPronouns" placeholder="e.g. she/her, they/them"/>
|
||||||
|
</label>
|
||||||
|
<label class="field field-wide">
|
||||||
|
<span>Appearance</span>
|
||||||
|
<textarea id="marketPersonaAppearance" rows="3" placeholder="Describe their look"></textarea>
|
||||||
|
</label>
|
||||||
|
<label class="field field-wide">
|
||||||
|
<span>Background</span>
|
||||||
|
<textarea id="marketPersonaBackground" rows="3" placeholder="Backstory, history, role, or origins"></textarea>
|
||||||
|
</label>
|
||||||
|
<label class="field field-wide">
|
||||||
|
<span>Details</span>
|
||||||
|
<textarea id="marketPersonaDetails" rows="4" placeholder="Personality traits, voice, goals, quirks, boundaries, style"></textarea>
|
||||||
|
</label>
|
||||||
|
<label class="field field-wide">
|
||||||
|
<span>Example dialogues</span>
|
||||||
|
<div class="dialogue-format-card" aria-hidden="true">
|
||||||
|
<p class="dialogue-format-label">Format</p>
|
||||||
|
<pre class="dialogue-format-example">{{character}}: Hello, my name is marc
|
||||||
|
{{user}}: Hi, Marc!
|
||||||
|
{{character}}: Nice to meet you</pre>
|
||||||
|
</div>
|
||||||
|
<textarea id="marketPersonaExampleDialogues" rows="5" placeholder="Write example exchanges that define the character's tone, phrasing, pacing, and personality."></textarea>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="persona-actions">
|
||||||
|
<button id="marketPersonaSave" type="submit" class="primary-action">Save AI Character</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<script src="market.js" defer></script>
|
<script src="market.js" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+334
-37
@@ -1,11 +1,37 @@
|
|||||||
const $ = (id) => document.getElementById(id);
|
const $ = (id) => document.getElementById(id);
|
||||||
const marketPersonaList = $("marketPersonaList"), marketUserPersonaList = $("marketUserPersonaList");
|
const marketPersonaList = $("marketPersonaList");
|
||||||
const refreshMarketBtn = $("refreshMarket"), marketStatus = $("marketStatus"), marketSearchInput = $("marketSearch");
|
const marketUserPersonaList = $("marketUserPersonaList");
|
||||||
const marketPreviewModal = $("marketPreviewModal"), marketPreviewName = $("marketPreviewName"), marketPreviewMeta = $("marketPreviewMeta");
|
const marketOwnedPersonaList = $("marketOwnedPersonaList");
|
||||||
const marketPreviewPronouns = $("marketPreviewPronouns"), marketPreviewAppearance = $("marketPreviewAppearance"), marketPreviewBackground = $("marketPreviewBackground"), marketPreviewDetails = $("marketPreviewDetails");
|
const refreshMarketBtn = $("refreshMarket");
|
||||||
const marketPreviewConfirm = $("marketPreviewConfirm"), marketPreviewCancel = $("marketPreviewCancel"), marketPreviewClose = $("marketPreviewClose");
|
const marketStatus = $("marketStatus");
|
||||||
|
const marketSearchInput = $("marketSearch");
|
||||||
|
const marketViewButtons = document.querySelectorAll("[data-market-view]");
|
||||||
|
const marketPanels = document.querySelectorAll("[data-market-panel]");
|
||||||
|
const marketCreatePersonaBtn = $("marketCreatePersona");
|
||||||
|
const marketPreviewModal = $("marketPreviewModal");
|
||||||
|
const marketPreviewName = $("marketPreviewName");
|
||||||
|
const marketPreviewMeta = $("marketPreviewMeta");
|
||||||
|
const marketPreviewPronouns = $("marketPreviewPronouns");
|
||||||
|
const marketPreviewAppearance = $("marketPreviewAppearance");
|
||||||
|
const marketPreviewBackground = $("marketPreviewBackground");
|
||||||
|
const marketPreviewDetails = $("marketPreviewDetails");
|
||||||
|
const marketPreviewExampleDialogues = $("marketPreviewExampleDialogues");
|
||||||
|
const marketPreviewUserPersonaField = $("marketPreviewUserPersonaField");
|
||||||
|
const marketPreviewUserPersonaSelect = $("marketPreviewUserPersonaSelect");
|
||||||
|
const marketPreviewConfirm = $("marketPreviewConfirm");
|
||||||
|
const marketPreviewClose = $("marketPreviewClose");
|
||||||
|
const marketPersonaModal = $("marketPersonaModal");
|
||||||
|
const marketPersonaClose = $("marketPersonaClose");
|
||||||
|
const marketPersonaForm = $("marketPersonaForm");
|
||||||
|
const marketPersonaFormTitle = $("marketPersonaFormTitle");
|
||||||
|
const marketPersonaFormNotice = $("marketPersonaFormNotice");
|
||||||
|
const marketPersonaName = $("marketPersonaName");
|
||||||
|
const marketPersonaPronouns = $("marketPersonaPronouns");
|
||||||
|
const marketPersonaAppearance = $("marketPersonaAppearance");
|
||||||
|
const marketPersonaBackground = $("marketPersonaBackground");
|
||||||
|
const marketPersonaDetails = $("marketPersonaDetails");
|
||||||
|
const marketPersonaExampleDialogues = $("marketPersonaExampleDialogues");
|
||||||
const themeNameTargets = document.querySelectorAll("[data-theme-name]");
|
const themeNameTargets = document.querySelectorAll("[data-theme-name]");
|
||||||
let allMarketPersonas = [], pendingMarketPersona = null;
|
|
||||||
|
|
||||||
const themes = {
|
const themes = {
|
||||||
"fakegpt": {name: "FakeGPT"},
|
"fakegpt": {name: "FakeGPT"},
|
||||||
@@ -16,9 +42,20 @@ const themes = {
|
|||||||
"confusity": {name: "Confusity"}
|
"confusity": {name: "Confusity"}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let allMarketPersonas = [];
|
||||||
|
let assistantPersonas = [];
|
||||||
|
let userPersonas = [];
|
||||||
|
let publishedPersonaIds = new Set();
|
||||||
|
let pendingMarketPersona = null;
|
||||||
|
let editingPersonaId = null;
|
||||||
|
|
||||||
async function request(url, data, method = "POST") {
|
async function request(url, data, method = "POST") {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url, {method, headers: {"Content-Type": "application/json"}, body: data ? JSON.stringify(data) : undefined});
|
const res = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: {"Content-Type": "application/json"},
|
||||||
|
body: data ? JSON.stringify(data) : undefined
|
||||||
|
});
|
||||||
return await res.json();
|
return await res.json();
|
||||||
} catch {
|
} catch {
|
||||||
return {error: "Network error - please try again."};
|
return {error: "Network error - please try again."};
|
||||||
@@ -27,12 +64,16 @@ async function request(url, data, method = "POST") {
|
|||||||
|
|
||||||
const get = (url) => request(url, null, "GET");
|
const get = (url) => request(url, null, "GET");
|
||||||
const post = (url, data) => request(url, data, "POST");
|
const post = (url, data) => request(url, data, "POST");
|
||||||
|
const put = (url, data) => request(url, data, "PUT");
|
||||||
|
const del = (url) => request(url, null, "DELETE");
|
||||||
|
|
||||||
function applyTheme(themeKey) {
|
function applyTheme(themeKey) {
|
||||||
const nextTheme = themes[themeKey] ? themeKey : "fakegpt";
|
const nextTheme = themes[themeKey] ? themeKey : "fakegpt";
|
||||||
document.body.dataset.theme = nextTheme;
|
document.body.dataset.theme = nextTheme;
|
||||||
document.title = `${themes[nextTheme].name} Market`;
|
document.title = `${themes[nextTheme].name} Market`;
|
||||||
themeNameTargets.forEach((target) => { target.textContent = themes[nextTheme].name; });
|
themeNameTargets.forEach((target) => {
|
||||||
|
target.textContent = themes[nextTheme].name;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setMarketStatus(message, state = "") {
|
function setMarketStatus(message, state = "") {
|
||||||
@@ -40,9 +81,28 @@ function setMarketStatus(message, state = "") {
|
|||||||
marketStatus.className = state ? `status persona-status ${state}` : "status persona-status";
|
marketStatus.className = state ? `status persona-status ${state}` : "status persona-status";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setPersonaFormNotice(message = "", state = "") {
|
||||||
|
if (!message) {
|
||||||
|
marketPersonaFormNotice.textContent = "";
|
||||||
|
marketPersonaFormNotice.className = "status hidden";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
marketPersonaFormNotice.textContent = message;
|
||||||
|
marketPersonaFormNotice.className = state ? `status ${state}` : "status";
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMarketView(view) {
|
||||||
|
marketViewButtons.forEach((item) => item.classList.toggle("active", item.dataset.marketView === view));
|
||||||
|
marketPanels.forEach((panel) => panel.classList.toggle("active", panel.dataset.marketPanel === view));
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
params.set("view", view);
|
||||||
|
history.replaceState(null, "", `${window.location.pathname}?${params.toString()}`);
|
||||||
|
}
|
||||||
|
|
||||||
function buildPersonaMeta(persona) {
|
function buildPersonaMeta(persona) {
|
||||||
|
const creator = persona.creator_username || "You";
|
||||||
const pronouns = persona.pronouns ? `Pronouns: ${persona.pronouns}` : "Pronouns: n/a";
|
const pronouns = persona.pronouns ? `Pronouns: ${persona.pronouns}` : "Pronouns: n/a";
|
||||||
return `${persona.creator_username} | ${pronouns}`;
|
return `${creator} | ${pronouns}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatPersonaField(value) {
|
function formatPersonaField(value) {
|
||||||
@@ -52,6 +112,74 @@ function formatPersonaField(value) {
|
|||||||
function closeMarketPreview() {
|
function closeMarketPreview() {
|
||||||
marketPreviewModal.classList.add("hidden");
|
marketPreviewModal.classList.add("hidden");
|
||||||
pendingMarketPersona = null;
|
pendingMarketPersona = null;
|
||||||
|
marketPreviewUserPersonaSelect.innerHTML = "";
|
||||||
|
marketPreviewConfirm.classList.add("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateUserPersonaChoices() {
|
||||||
|
marketPreviewUserPersonaSelect.innerHTML = "";
|
||||||
|
|
||||||
|
const promptOption = document.createElement("option");
|
||||||
|
promptOption.value = "";
|
||||||
|
promptOption.textContent = "Choose who you are";
|
||||||
|
promptOption.disabled = true;
|
||||||
|
promptOption.selected = true;
|
||||||
|
marketPreviewUserPersonaSelect.appendChild(promptOption);
|
||||||
|
|
||||||
|
const selfOption = document.createElement("option");
|
||||||
|
selfOption.value = "self";
|
||||||
|
selfOption.textContent = "Yourself (no persona)";
|
||||||
|
marketPreviewUserPersonaSelect.appendChild(selfOption);
|
||||||
|
|
||||||
|
userPersonas.forEach((persona) => {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = String(persona.id);
|
||||||
|
option.textContent = persona.name;
|
||||||
|
marketPreviewUserPersonaSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
const createOption = document.createElement("option");
|
||||||
|
createOption.value = "create_new";
|
||||||
|
createOption.textContent = "Create new persona";
|
||||||
|
marketPreviewUserPersonaSelect.appendChild(createOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openMarketPreview(persona) {
|
||||||
|
pendingMarketPersona = persona;
|
||||||
|
marketPreviewName.textContent = persona.name;
|
||||||
|
marketPreviewMeta.textContent = buildPersonaMeta(persona);
|
||||||
|
marketPreviewPronouns.textContent = formatPersonaField(persona.pronouns);
|
||||||
|
marketPreviewAppearance.textContent = formatPersonaField(persona.appearance);
|
||||||
|
marketPreviewBackground.textContent = formatPersonaField(persona.background);
|
||||||
|
marketPreviewDetails.textContent = formatPersonaField(persona.details);
|
||||||
|
marketPreviewExampleDialogues.textContent = formatPersonaField(persona.example_dialogues);
|
||||||
|
|
||||||
|
const isAssistant = persona.persona_type === "assistant";
|
||||||
|
marketPreviewUserPersonaField.classList.toggle("hidden", !isAssistant);
|
||||||
|
marketPreviewConfirm.textContent = isAssistant ? "Start chat" : "Collect as user persona";
|
||||||
|
marketPreviewConfirm.classList.toggle("hidden", isAssistant);
|
||||||
|
if (isAssistant) populateUserPersonaChoices();
|
||||||
|
marketPreviewModal.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPersonaForm(persona = null) {
|
||||||
|
editingPersonaId = persona ? persona.id : null;
|
||||||
|
marketPersonaFormTitle.textContent = persona ? "Edit AI Character" : "Create AI Character";
|
||||||
|
marketPersonaName.value = persona?.name || "";
|
||||||
|
marketPersonaPronouns.value = persona?.pronouns || "";
|
||||||
|
marketPersonaAppearance.value = persona?.appearance || "";
|
||||||
|
marketPersonaBackground.value = persona?.background || "";
|
||||||
|
marketPersonaDetails.value = persona?.details || "";
|
||||||
|
marketPersonaExampleDialogues.value = persona?.example_dialogues || "";
|
||||||
|
setPersonaFormNotice("");
|
||||||
|
marketPersonaModal.classList.remove("hidden");
|
||||||
|
marketPersonaName.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePersonaForm() {
|
||||||
|
marketPersonaModal.classList.add("hidden");
|
||||||
|
editingPersonaId = null;
|
||||||
|
setPersonaFormNotice("");
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMarketList(items, listElement, type) {
|
function renderMarketList(items, listElement, type) {
|
||||||
@@ -59,7 +187,7 @@ function renderMarketList(items, listElement, type) {
|
|||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
const empty = document.createElement("p");
|
const empty = document.createElement("p");
|
||||||
empty.className = "status persona-status";
|
empty.className = "status persona-status";
|
||||||
empty.textContent = type === "assistant" ? "No AI personas found." : "No user personas found.";
|
empty.textContent = type === "assistant" ? "No AI Characters found." : "No User Personas found.";
|
||||||
listElement.appendChild(empty);
|
listElement.appendChild(empty);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -77,9 +205,10 @@ function renderMarketList(items, listElement, type) {
|
|||||||
titleRow.className = "persona-title-row";
|
titleRow.className = "persona-title-row";
|
||||||
actions.className = "persona-item-actions";
|
actions.className = "persona-item-actions";
|
||||||
tag.className = `persona-role-tag ${type === "assistant" ? "ai" : "user"}`;
|
tag.className = `persona-role-tag ${type === "assistant" ? "ai" : "user"}`;
|
||||||
tag.textContent = type === "assistant" ? "AI" : "You";
|
tag.textContent = type === "assistant" ? "AI Character" : "User";
|
||||||
title.textContent = persona.name;
|
title.textContent = persona.name;
|
||||||
meta.textContent = buildPersonaMeta(persona);
|
meta.textContent = buildPersonaMeta(persona);
|
||||||
|
|
||||||
detailsBtn.type = "button";
|
detailsBtn.type = "button";
|
||||||
detailsBtn.textContent = "View";
|
detailsBtn.textContent = "View";
|
||||||
detailsBtn.addEventListener("click", () => openMarketPreview(persona));
|
detailsBtn.addEventListener("click", () => openMarketPreview(persona));
|
||||||
@@ -91,11 +220,63 @@ function renderMarketList(items, listElement, type) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderOwnedPersonaList() {
|
||||||
|
marketOwnedPersonaList.innerHTML = "";
|
||||||
|
if (!assistantPersonas.length) {
|
||||||
|
const empty = document.createElement("p");
|
||||||
|
empty.className = "status persona-status";
|
||||||
|
empty.textContent = "No AI Characters yet. Create one to get started.";
|
||||||
|
marketOwnedPersonaList.appendChild(empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
assistantPersonas.forEach((persona) => {
|
||||||
|
const item = document.createElement("div");
|
||||||
|
const titleRow = document.createElement("div");
|
||||||
|
const title = document.createElement("h4");
|
||||||
|
const tag = document.createElement("span");
|
||||||
|
const meta = document.createElement("p");
|
||||||
|
const actions = document.createElement("div");
|
||||||
|
|
||||||
|
item.className = "persona-item";
|
||||||
|
titleRow.className = "persona-title-row";
|
||||||
|
actions.className = "persona-item-actions";
|
||||||
|
tag.className = "persona-role-tag ai";
|
||||||
|
tag.textContent = "AI Character";
|
||||||
|
title.textContent = persona.name;
|
||||||
|
meta.textContent = formatPersonaField(persona.pronouns) === "Not provided." ? "Pronouns: n/a" : `Pronouns: ${persona.pronouns}`;
|
||||||
|
|
||||||
|
[
|
||||||
|
["Edit", () => openPersonaForm(persona)],
|
||||||
|
[publishedPersonaIds.has(persona.id) ? "Update listing" : "Publish", () => publishPersona(persona.id)],
|
||||||
|
["Delete", () => deletePersona(persona.id)]
|
||||||
|
].forEach(([label, handler]) => {
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.type = "button";
|
||||||
|
button.textContent = label;
|
||||||
|
button.addEventListener("click", handler);
|
||||||
|
actions.appendChild(button);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (publishedPersonaIds.has(persona.id)) {
|
||||||
|
const unpublish = document.createElement("button");
|
||||||
|
unpublish.type = "button";
|
||||||
|
unpublish.textContent = "Unpublish";
|
||||||
|
unpublish.addEventListener("click", () => unpublishPersona(persona.id));
|
||||||
|
actions.appendChild(unpublish);
|
||||||
|
}
|
||||||
|
|
||||||
|
titleRow.append(title, tag);
|
||||||
|
item.append(titleRow, meta, actions);
|
||||||
|
marketOwnedPersonaList.appendChild(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function filterMarketPersonas() {
|
function filterMarketPersonas() {
|
||||||
const query = marketSearchInput.value.trim().toLowerCase();
|
const query = marketSearchInput.value.trim().toLowerCase();
|
||||||
if (!query) return allMarketPersonas;
|
if (!query) return allMarketPersonas;
|
||||||
return allMarketPersonas.filter((persona) =>
|
return allMarketPersonas.filter((persona) =>
|
||||||
[persona.name, persona.creator_username, persona.pronouns, persona.appearance, persona.background, persona.details]
|
[persona.name, persona.creator_username, persona.pronouns, persona.appearance, persona.background, persona.details, persona.example_dialogues]
|
||||||
.some((field) => field && field.toLowerCase().includes(query))
|
.some((field) => field && field.toLowerCase().includes(query))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -104,6 +285,8 @@ function applyMarketFilter() {
|
|||||||
const filtered = filterMarketPersonas();
|
const filtered = filterMarketPersonas();
|
||||||
renderMarketList(filtered.filter((persona) => persona.persona_type === "assistant"), marketPersonaList, "assistant");
|
renderMarketList(filtered.filter((persona) => persona.persona_type === "assistant"), marketPersonaList, "assistant");
|
||||||
renderMarketList(filtered.filter((persona) => persona.persona_type === "user"), marketUserPersonaList, "user");
|
renderMarketList(filtered.filter((persona) => persona.persona_type === "user"), marketUserPersonaList, "user");
|
||||||
|
renderOwnedPersonaList();
|
||||||
|
|
||||||
if (marketSearchInput.value.trim() && !filtered.length) {
|
if (marketSearchInput.value.trim() && !filtered.length) {
|
||||||
setMarketStatus("No personas match your search.", "error");
|
setMarketStatus("No personas match your search.", "error");
|
||||||
return;
|
return;
|
||||||
@@ -111,48 +294,162 @@ function applyMarketFilter() {
|
|||||||
setMarketStatus(`${filtered.length} persona${filtered.length === 1 ? "" : "s"} shown.`);
|
setMarketStatus(`${filtered.length} persona${filtered.length === 1 ? "" : "s"} shown.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openMarketPreview(persona) {
|
async function loadOwnedPersonas() {
|
||||||
pendingMarketPersona = persona;
|
const res = await get("/personas");
|
||||||
marketPreviewName.textContent = persona.name;
|
if (res.error) return false;
|
||||||
marketPreviewMeta.textContent = buildPersonaMeta(persona);
|
assistantPersonas = res.assistantPersonas || [];
|
||||||
marketPreviewPronouns.textContent = formatPersonaField(persona.pronouns);
|
userPersonas = res.userPersonas || [];
|
||||||
marketPreviewAppearance.textContent = formatPersonaField(persona.appearance);
|
publishedPersonaIds = new Set(res.publishedPersonaIds || []);
|
||||||
marketPreviewBackground.textContent = formatPersonaField(persona.background);
|
return true;
|
||||||
marketPreviewDetails.textContent = formatPersonaField(persona.details);
|
|
||||||
marketPreviewConfirm.textContent = persona.persona_type === "assistant" ? "Collect as AI persona" : "Collect as user persona";
|
|
||||||
marketPreviewModal.classList.remove("hidden");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadMarketPersonas() {
|
async function loadMarketPersonas() {
|
||||||
setMarketStatus("Loading market personas...");
|
setMarketStatus("Loading personas...");
|
||||||
const res = await get("/personas/market");
|
const [marketRes, ownedOk] = await Promise.all([get("/personas/market"), loadOwnedPersonas()]);
|
||||||
if (res.error) return setMarketStatus(res.error, "error");
|
if (marketRes.error) {
|
||||||
allMarketPersonas = res.personas || [];
|
setMarketStatus(marketRes.error, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!ownedOk) {
|
||||||
|
setMarketStatus("Unable to load your personas.", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
allMarketPersonas = marketRes.personas || [];
|
||||||
applyMarketFilter();
|
applyMarketFilter();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function collectMarketPersona(marketId, personaType) {
|
async function collectMarketPersona(marketId) {
|
||||||
setMarketStatus(personaType === "assistant" ? "Collecting AI persona..." : "Collecting user persona...");
|
setMarketStatus("Collecting User Persona...");
|
||||||
const res = await post(`/personas/market/${marketId}/collect`, {equip: true});
|
const res = await post(`/personas/market/${marketId}/collect`, {equip: true});
|
||||||
if (res.error) return setMarketStatus(res.error, "error");
|
if (res.error) {
|
||||||
|
setMarketStatus(res.error, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
await loadMarketPersonas();
|
await loadMarketPersonas();
|
||||||
setMarketStatus(personaType === "assistant" ? "AI persona collected." : "User persona collected.", "success");
|
setMarketStatus("User Persona collected.", "success");
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshMarketBtn.addEventListener("click", () => { void loadMarketPersonas(); });
|
async function startMarketPersonaChat(marketId, userPersonaSelection) {
|
||||||
|
if (userPersonaSelection === "create_new") {
|
||||||
|
window.location.href = "/settings?view=my-personas&create=user";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setMarketStatus("Starting AI Character chat...");
|
||||||
|
const userPersonaId = userPersonaSelection === "self" ? null : Number(userPersonaSelection);
|
||||||
|
const res = await post(`/personas/market/${marketId}/chat`, {userPersonaId});
|
||||||
|
if (res.error || !res.chat) {
|
||||||
|
setMarketStatus(res.error || "Unable to start chat.", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.location.href = `/?chat=${res.chat.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function savePersona() {
|
||||||
|
const payload = {
|
||||||
|
personaType: "assistant",
|
||||||
|
name: marketPersonaName.value.trim(),
|
||||||
|
pronouns: marketPersonaPronouns.value.trim(),
|
||||||
|
appearance: marketPersonaAppearance.value.trim(),
|
||||||
|
background: marketPersonaBackground.value.trim(),
|
||||||
|
details: marketPersonaDetails.value.trim(),
|
||||||
|
exampleDialogues: marketPersonaExampleDialogues.value.trim()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!payload.name) {
|
||||||
|
setPersonaFormNotice("Character name is required.", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = editingPersonaId
|
||||||
|
? await put(`/personas/${editingPersonaId}`, payload)
|
||||||
|
: await post("/personas", payload);
|
||||||
|
|
||||||
|
if (res.error) {
|
||||||
|
setPersonaFormNotice(res.error, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadMarketPersonas();
|
||||||
|
closePersonaForm();
|
||||||
|
setMarketView("my-personas");
|
||||||
|
setMarketStatus(editingPersonaId ? "AI Character updated." : "AI Character created.", "success");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deletePersona(id) {
|
||||||
|
const res = await del(`/personas/${id}`);
|
||||||
|
if (res.error) {
|
||||||
|
setMarketStatus(res.error, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await loadMarketPersonas();
|
||||||
|
setMarketStatus("AI Character deleted.", "success");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function publishPersona(id) {
|
||||||
|
const res = await post(`/personas/${id}/publish`, {});
|
||||||
|
if (res.error) {
|
||||||
|
setMarketStatus(res.error, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await loadMarketPersonas();
|
||||||
|
setMarketStatus("AI Character published.", "success");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unpublishPersona(id) {
|
||||||
|
const res = await post(`/personas/${id}/unpublish`, {});
|
||||||
|
if (res.error) {
|
||||||
|
setMarketStatus(res.error, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await loadMarketPersonas();
|
||||||
|
setMarketStatus("AI Character unpublished.", "success");
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshMarketBtn.addEventListener("click", () => {
|
||||||
|
void loadMarketPersonas();
|
||||||
|
});
|
||||||
marketSearchInput.addEventListener("input", applyMarketFilter);
|
marketSearchInput.addEventListener("input", applyMarketFilter);
|
||||||
marketPreviewCancel.addEventListener("click", closeMarketPreview);
|
marketViewButtons.forEach((button) => {
|
||||||
|
button.addEventListener("click", () => setMarketView(button.dataset.marketView));
|
||||||
|
});
|
||||||
|
marketCreatePersonaBtn.addEventListener("click", () => openPersonaForm());
|
||||||
marketPreviewClose.addEventListener("click", closeMarketPreview);
|
marketPreviewClose.addEventListener("click", closeMarketPreview);
|
||||||
marketPreviewModal.addEventListener("click", (event) => { if (event.target === marketPreviewModal) closeMarketPreview(); });
|
marketPreviewModal.addEventListener("click", (event) => {
|
||||||
|
if (event.target === marketPreviewModal) closeMarketPreview();
|
||||||
|
});
|
||||||
|
marketPreviewUserPersonaSelect.addEventListener("change", () => {
|
||||||
|
if (!pendingMarketPersona || pendingMarketPersona.persona_type !== "assistant") return;
|
||||||
|
if (marketPreviewUserPersonaSelect.value === "create_new") {
|
||||||
|
window.location.href = "/settings?view=my-personas&create=user";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
marketPreviewConfirm.classList.toggle("hidden", !marketPreviewUserPersonaSelect.value);
|
||||||
|
});
|
||||||
marketPreviewConfirm.addEventListener("click", async () => {
|
marketPreviewConfirm.addEventListener("click", async () => {
|
||||||
if (!pendingMarketPersona) return;
|
if (!pendingMarketPersona) return;
|
||||||
const marketId = pendingMarketPersona.id;
|
const marketId = pendingMarketPersona.id;
|
||||||
const personaType = pendingMarketPersona.persona_type;
|
if (pendingMarketPersona.persona_type === "assistant") {
|
||||||
|
await startMarketPersonaChat(marketId, marketPreviewUserPersonaSelect.value || "");
|
||||||
|
closeMarketPreview();
|
||||||
|
return;
|
||||||
|
}
|
||||||
closeMarketPreview();
|
closeMarketPreview();
|
||||||
await collectMarketPersona(marketId, personaType);
|
await collectMarketPersona(marketId);
|
||||||
|
});
|
||||||
|
marketPersonaClose.addEventListener("click", closePersonaForm);
|
||||||
|
marketPersonaModal.addEventListener("click", (event) => {
|
||||||
|
if (event.target === marketPersonaModal) closePersonaForm();
|
||||||
|
});
|
||||||
|
marketPersonaForm.addEventListener("submit", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
void savePersona();
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener("load", () => {
|
window.addEventListener("load", async () => {
|
||||||
applyTheme(localStorage.getItem("krishd-theme") || "fakegpt");
|
applyTheme(localStorage.getItem("krishd-theme") || "fakegpt");
|
||||||
void loadMarketPersonas();
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
setMarketView(params.get("view") || "ai-characters");
|
||||||
|
await loadMarketPersonas();
|
||||||
|
if (params.get("create") === "assistant") openPersonaForm();
|
||||||
});
|
});
|
||||||
|
|||||||
+172
-32
@@ -11,25 +11,30 @@ 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 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");
|
||||||
const personaBackgroundInput = $("personaBackground"), personaDetailsInput = $("personaDetails");
|
const personaBackgroundInput = $("personaBackground"), personaDetailsInput = $("personaDetails");
|
||||||
const personaExamplesField = $("personaExamplesField"), personaExampleDialoguesInput = $("personaExampleDialogues");
|
const personaExamplesField = $("personaExamplesField"), personaExampleDialoguesInput = $("personaExampleDialogues");
|
||||||
const personaFormNotice = $("personaFormNotice");
|
const personaFormNotice = $("personaFormNotice");
|
||||||
const newPersonaBtn = $("newPersona"), newUserPersonaBtn = $("newUserPersona"), clearPersonaBtn = $("clearPersona"), clearUserPersonaBtn = $("clearUserPersona");
|
const roleplayNewPersonaBtn = $("roleplayNewPersona"), clearUserPersonaBtn = $("clearUserPersona");
|
||||||
const activePersonaStatus = $("activePersonaStatus"), activeUserPersonaStatus = $("activeUserPersonaStatus");
|
const activePersonaStatus = $("activePersonaStatus"), activeUserPersonaStatus = $("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 roleplayStarterNotice = $("roleplayStarterNotice"), roleplayCharacterSelect = $("roleplayCharacterSelect"), roleplayUserPersonaSelect = $("roleplayUserPersonaSelect");
|
||||||
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 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;
|
||||||
|
|
||||||
let isProcessing = false, activeChatId = null, editingPersonaId = null, editingPersonaType = "assistant", currentUsername = "";
|
let isProcessing = false, activeChatId = null, editingPersonaId = null, editingPersonaType = "assistant", currentUsername = "";
|
||||||
let chatSessions = [], currentMessages = [], assistantPersonas = [], userPersonas = [];
|
let chatSessions = [], currentMessages = [], assistantPersonas = [], userPersonas = [];
|
||||||
let activePersonaId = null, 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;
|
||||||
|
|
||||||
const themes = {
|
const themes = {
|
||||||
"fakegpt": {name: "FakeGPT", short: "FG"},
|
"fakegpt": {name: "FakeGPT", short: "FG"},
|
||||||
@@ -112,6 +117,7 @@ function addMessage(content, isUser = false, isLoading = false) {
|
|||||||
function renderMessages() {
|
function renderMessages() {
|
||||||
messagesDiv.innerHTML = "";
|
messagesDiv.innerHTML = "";
|
||||||
currentMessages.forEach((msg) => addMessage(msg.content, msg.role === "user"));
|
currentMessages.forEach((msg) => addMessage(msg.content, msg.role === "user"));
|
||||||
|
updateChatParticipants();
|
||||||
updateChatActionState();
|
updateChatActionState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,6 +129,45 @@ function updateWorkspaceCopy() {
|
|||||||
if (sessionUser) {
|
if (sessionUser) {
|
||||||
sessionUser.textContent = currentUsername || "-";
|
sessionUser.textContent = currentUsername || "-";
|
||||||
}
|
}
|
||||||
|
updateChatParticipants();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRoleplayStarterNotice(message = "", state = "") {
|
||||||
|
if (!roleplayStarterNotice) return;
|
||||||
|
if (!message) {
|
||||||
|
roleplayStarterNotice.textContent = "";
|
||||||
|
roleplayStarterNotice.className = "status hidden";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
roleplayStarterNotice.textContent = message;
|
||||||
|
roleplayStarterNotice.className = state ? `status ${state}` : "status";
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateChatParticipants() {
|
||||||
|
const chat = getChatById(activeChatId);
|
||||||
|
if (!chatModePill || !chatCharacterPill || !chatUserPersonaPill) return;
|
||||||
|
if (!chat) {
|
||||||
|
chatModePill.textContent = "No chat selected";
|
||||||
|
chatCharacterPill.classList.add("hidden");
|
||||||
|
chatUserPersonaPill.classList.add("hidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (chat.assistant_persona_id) {
|
||||||
|
chatModePill.textContent = "Roleplay mode";
|
||||||
|
chatCharacterPill.textContent = `Character: ${chat.assistant_persona_name || chat.title}`;
|
||||||
|
chatCharacterPill.classList.remove("hidden");
|
||||||
|
if (chat.user_persona_id) {
|
||||||
|
chatUserPersonaPill.textContent = `You as: ${chat.user_persona_name || "User persona"}`;
|
||||||
|
chatUserPersonaPill.classList.remove("hidden");
|
||||||
|
} else {
|
||||||
|
chatUserPersonaPill.textContent = "You as: yourself";
|
||||||
|
chatUserPersonaPill.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chatModePill.textContent = "Assistant mode";
|
||||||
|
chatCharacterPill.classList.add("hidden");
|
||||||
|
chatUserPersonaPill.classList.add("hidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateContextRail() {
|
function updateContextRail() {
|
||||||
@@ -203,8 +248,9 @@ function applyTheme(themeKey, persist = true) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateChatActionState() {
|
function updateChatActionState() {
|
||||||
const hasChat = Boolean(activeChatId);
|
const activeChat = getChatById(activeChatId);
|
||||||
renameChatBtn.disabled = !hasChat;
|
const hasChat = Boolean(activeChat);
|
||||||
|
renameChatBtn.disabled = !hasChat || Boolean(activeChat?.assistant_persona_id);
|
||||||
clearChatBtn.disabled = !hasChat;
|
clearChatBtn.disabled = !hasChat;
|
||||||
exportChatBtn.disabled = !hasChat || currentMessages.length === 0;
|
exportChatBtn.disabled = !hasChat || currentMessages.length === 0;
|
||||||
}
|
}
|
||||||
@@ -221,11 +267,18 @@ function renderChatList() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
filtered.forEach((chat) => {
|
filtered.forEach((chat) => {
|
||||||
const item = document.createElement("div"), title = document.createElement("span"), remove = document.createElement("button");
|
const item = document.createElement("div"), content = document.createElement("div"), title = document.createElement("span"), meta = document.createElement("small"), remove = document.createElement("button");
|
||||||
item.className = `chat-item${chat.id === activeChatId ? " active" : ""}`;
|
item.className = `chat-item${chat.id === activeChatId ? " active" : ""}`;
|
||||||
title.textContent = chat.title; remove.type = "button"; remove.textContent = "Delete";
|
content.className = "chat-item-copy";
|
||||||
|
title.textContent = chat.title;
|
||||||
|
meta.className = "chat-item-meta";
|
||||||
|
meta.textContent = chat.assistant_persona_id
|
||||||
|
? `Roleplay${chat.user_persona_name ? ` · ${chat.user_persona_name}` : ""}`
|
||||||
|
: "Assistant chat";
|
||||||
|
remove.type = "button"; remove.textContent = "Delete";
|
||||||
remove.addEventListener("click", async (event) => { event.stopPropagation(); await deleteChat(chat.id); });
|
remove.addEventListener("click", async (event) => { event.stopPropagation(); await deleteChat(chat.id); });
|
||||||
item.append(title, remove);
|
content.append(title, meta);
|
||||||
|
item.append(content, remove);
|
||||||
item.addEventListener("click", () => setActiveChat(chat.id));
|
item.addEventListener("click", () => setActiveChat(chat.id));
|
||||||
chatList.appendChild(item);
|
chatList.appendChild(item);
|
||||||
});
|
});
|
||||||
@@ -237,10 +290,13 @@ function updateActivePersonaStatus(items, activeId, statusEl, emptyText, prefix)
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setActivePersonaStatus() {
|
function setActivePersonaStatus() {
|
||||||
updateActivePersonaStatus(assistantPersonas, activePersonaId, activePersonaStatus, "No AI persona equipped.", "AI persona equipped");
|
if (activePersonaStatus) {
|
||||||
updateActivePersonaStatus(userPersonas, activeUserPersonaId, activeUserPersonaStatus, "No user persona set.", "User persona active");
|
activePersonaStatus.textContent = assistantPersonas.length
|
||||||
clearPersonaBtn.disabled = !activePersonaId;
|
? "Pick a character, choose a user persona, and start the roleplay."
|
||||||
clearUserPersonaBtn.disabled = !activeUserPersonaId;
|
: "No characters yet. Create one to start a roleplay.";
|
||||||
|
}
|
||||||
|
updateActivePersonaStatus(userPersonas, activeUserPersonaId, activeUserPersonaStatus, "No default user persona set.", "Default user persona");
|
||||||
|
if (clearUserPersonaBtn) clearUserPersonaBtn.disabled = !activeUserPersonaId;
|
||||||
updateContextRail();
|
updateContextRail();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,14 +306,14 @@ function openPersonaForm(persona = null, personaType = "assistant") {
|
|||||||
personaModal.classList.remove("hidden");
|
personaModal.classList.remove("hidden");
|
||||||
if (persona) {
|
if (persona) {
|
||||||
editingPersonaId = persona.id; editingPersonaType = persona.persona_type || "assistant";
|
editingPersonaId = persona.id; editingPersonaType = persona.persona_type || "assistant";
|
||||||
personaFormTitle.textContent = `Edit ${editingPersonaType === "user" ? "your persona" : "AI persona"}`;
|
personaFormTitle.textContent = `Edit ${editingPersonaType === "user" ? "your persona" : "AI Character"}`;
|
||||||
personaTypeSelect.value = editingPersonaType; personaTypeSelect.disabled = true;
|
personaTypeSelect.value = editingPersonaType; personaTypeSelect.disabled = true;
|
||||||
personaNameInput.value = persona.name || ""; personaPronounsInput.value = persona.pronouns || "";
|
personaNameInput.value = persona.name || ""; personaPronounsInput.value = persona.pronouns || "";
|
||||||
personaAppearanceInput.value = persona.appearance || ""; personaBackgroundInput.value = persona.background || ""; personaDetailsInput.value = persona.details || "";
|
personaAppearanceInput.value = persona.appearance || ""; personaBackgroundInput.value = persona.background || ""; personaDetailsInput.value = persona.details || "";
|
||||||
personaExampleDialoguesInput.value = persona.example_dialogues || "";
|
personaExampleDialoguesInput.value = persona.example_dialogues || "";
|
||||||
} else {
|
} else {
|
||||||
editingPersonaId = null; editingPersonaType = personaType;
|
editingPersonaId = null; editingPersonaType = personaType;
|
||||||
personaFormTitle.textContent = `Create ${personaType === "user" ? "your persona" : "AI persona"}`;
|
personaFormTitle.textContent = `Create ${personaType === "user" ? "your persona" : "AI Character"}`;
|
||||||
personaTypeSelect.value = personaType; personaTypeSelect.disabled = false;
|
personaTypeSelect.value = personaType; personaTypeSelect.disabled = false;
|
||||||
personaNameInput.value = ""; personaPronounsInput.value = ""; personaAppearanceInput.value = ""; personaBackgroundInput.value = ""; personaDetailsInput.value = "";
|
personaNameInput.value = ""; personaPronounsInput.value = ""; personaAppearanceInput.value = ""; personaBackgroundInput.value = ""; personaDetailsInput.value = "";
|
||||||
personaExampleDialoguesInput.value = "";
|
personaExampleDialoguesInput.value = "";
|
||||||
@@ -269,6 +325,45 @@ function openPersonaForm(persona = null, personaType = "assistant") {
|
|||||||
|
|
||||||
function closePersonaForm() { personaModal.classList.add("hidden"); editingPersonaId = null; editingPersonaType = "assistant"; personaTypeSelect.disabled = false; setPersonaFormNotice(""); }
|
function closePersonaForm() { personaModal.classList.add("hidden"); editingPersonaId = null; editingPersonaType = "assistant"; personaTypeSelect.disabled = false; setPersonaFormNotice(""); }
|
||||||
function closePersonaPopover() { personaPopover.classList.add("hidden"); personaMenuButton.setAttribute("aria-expanded", "false"); }
|
function closePersonaPopover() { personaPopover.classList.add("hidden"); personaMenuButton.setAttribute("aria-expanded", "false"); }
|
||||||
|
function populateRoleplayStarter() {
|
||||||
|
if (!roleplayCharacterSelect || !roleplayUserPersonaSelect) return;
|
||||||
|
roleplayCharacterSelect.innerHTML = "";
|
||||||
|
assistantPersonas.forEach((persona) => {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = String(persona.id);
|
||||||
|
option.textContent = persona.name;
|
||||||
|
option.selected = persona.id === roleplayPresetCharacterId;
|
||||||
|
roleplayCharacterSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
roleplayUserPersonaSelect.innerHTML = "";
|
||||||
|
const noneOption = document.createElement("option");
|
||||||
|
noneOption.value = "";
|
||||||
|
noneOption.textContent = "No user persona";
|
||||||
|
roleplayUserPersonaSelect.appendChild(noneOption);
|
||||||
|
userPersonas.forEach((persona) => {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = String(persona.id);
|
||||||
|
option.textContent = persona.name;
|
||||||
|
option.selected = persona.id === activeUserPersonaId;
|
||||||
|
roleplayUserPersonaSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function openRoleplayStarter(characterId = null) {
|
||||||
|
closePersonaPopover();
|
||||||
|
closeModelPopover();
|
||||||
|
roleplayPresetCharacterId = characterId;
|
||||||
|
populateRoleplayStarter();
|
||||||
|
setRoleplayStarterNotice("");
|
||||||
|
roleplayStarterModal.classList.remove("hidden");
|
||||||
|
if (roleplayCharacterSelect.options.length) roleplayCharacterSelect.focus();
|
||||||
|
}
|
||||||
|
function closeRoleplayStarter() {
|
||||||
|
if (!roleplayStarterModal) return;
|
||||||
|
roleplayStarterModal.classList.add("hidden");
|
||||||
|
roleplayPresetCharacterId = null;
|
||||||
|
roleplayStarterConfirm.disabled = false;
|
||||||
|
setRoleplayStarterNotice("");
|
||||||
|
}
|
||||||
function closeModelPopover() {
|
function closeModelPopover() {
|
||||||
modelPopover.classList.add("hidden");
|
modelPopover.classList.add("hidden");
|
||||||
modelMenuButton.setAttribute("aria-expanded", "false");
|
modelMenuButton.setAttribute("aria-expanded", "false");
|
||||||
@@ -330,6 +425,10 @@ function promptPopup(options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderPersonaList(items, activeId, listElement, personaType) {
|
function renderPersonaList(items, activeId, listElement, personaType) {
|
||||||
|
if (!listElement) {
|
||||||
|
setActivePersonaStatus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
listElement.innerHTML = "";
|
listElement.innerHTML = "";
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
const empty = document.createElement("p");
|
const empty = document.createElement("p");
|
||||||
@@ -342,10 +441,10 @@ function renderPersonaList(items, activeId, listElement, personaType) {
|
|||||||
items.forEach((persona) => {
|
items.forEach((persona) => {
|
||||||
const item = document.createElement("div"), titleRow = document.createElement("div"), title = document.createElement("h4");
|
const item = document.createElement("div"), titleRow = document.createElement("div"), title = document.createElement("h4");
|
||||||
const tag = document.createElement("span"), meta = document.createElement("p"), actions = document.createElement("div");
|
const tag = document.createElement("span"), meta = document.createElement("p"), actions = document.createElement("div");
|
||||||
item.className = `persona-item${persona.id === activeId ? " active" : ""}`; titleRow.className = "persona-title-row"; actions.className = "persona-item-actions";
|
item.className = `persona-item${personaType === "user" && persona.id === activeId ? " active" : ""}`; titleRow.className = "persona-title-row"; actions.className = "persona-item-actions";
|
||||||
tag.className = `persona-role-tag ${personaType === "user" ? "user" : "ai"}`; tag.textContent = personaType === "user" ? "You" : "AI";
|
tag.className = `persona-role-tag ${personaType === "user" ? "user" : "ai"}`; tag.textContent = personaType === "user" ? "You" : "AI";
|
||||||
title.textContent = persona.name; meta.textContent = persona.pronouns ? `Pronouns: ${persona.pronouns}` : "Pronouns: n/a";
|
title.textContent = persona.name; meta.textContent = persona.pronouns ? `Pronouns: ${persona.pronouns}` : "Pronouns: n/a";
|
||||||
[["Equip", () => equipPersona(persona.id, personaType), persona.id === activeId],
|
[[personaType === "assistant" ? "Start roleplay" : "Set default", () => equipPersona(persona.id, personaType), personaType === "user" && persona.id === activeId],
|
||||||
["Edit", () => openPersonaForm(persona), false],
|
["Edit", () => openPersonaForm(persona), false],
|
||||||
[publishedPersonaIds.has(persona.id) ? "Update listing" : "Publish", () => publishPersona(persona.id), false],
|
[publishedPersonaIds.has(persona.id) ? "Update listing" : "Publish", () => publishPersona(persona.id), false],
|
||||||
["Delete", () => deletePersona(persona.id), false]]
|
["Delete", () => deletePersona(persona.id), false]]
|
||||||
@@ -371,26 +470,30 @@ async function loadPersonas() {
|
|||||||
const res = await get("/personas");
|
const res = await get("/personas");
|
||||||
if (res.error) return setNotice(res.error, "error");
|
if (res.error) return setNotice(res.error, "error");
|
||||||
assistantPersonas = res.assistantPersonas || []; userPersonas = res.userPersonas || [];
|
assistantPersonas = res.assistantPersonas || []; userPersonas = res.userPersonas || [];
|
||||||
activePersonaId = res.activePersonaId || null; activeUserPersonaId = res.activeUserPersonaId || null; publishedPersonaIds = new Set(res.publishedPersonaIds || []);
|
activeUserPersonaId = res.activeUserPersonaId || null; publishedPersonaIds = new Set(res.publishedPersonaIds || []);
|
||||||
renderPersonaList(assistantPersonas, activePersonaId, personaList, "assistant");
|
|
||||||
renderPersonaList(userPersonas, activeUserPersonaId, userPersonaList, "user");
|
renderPersonaList(userPersonas, activeUserPersonaId, userPersonaList, "user");
|
||||||
|
populateRoleplayStarter();
|
||||||
|
setActivePersonaStatus();
|
||||||
updateWorkspaceCopy();
|
updateWorkspaceCopy();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function equipPersona(id, type) {
|
async function equipPersona(id, type) {
|
||||||
const res = await post(type === "user" ? `/personas/${id}/equip-user` : `/personas/${id}/equip`, {});
|
if (type === "assistant") {
|
||||||
|
openRoleplayStarter(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const res = await post(`/personas/${id}/equip-user`, {});
|
||||||
if (res.error) return setNotice(res.error, "error");
|
if (res.error) return setNotice(res.error, "error");
|
||||||
if (type === "user") activeUserPersonaId = id; else activePersonaId = id;
|
activeUserPersonaId = id;
|
||||||
renderPersonaList(assistantPersonas, activePersonaId, personaList, "assistant");
|
|
||||||
renderPersonaList(userPersonas, activeUserPersonaId, userPersonaList, "user");
|
renderPersonaList(userPersonas, activeUserPersonaId, userPersonaList, "user");
|
||||||
setNotice(type === "user" ? "User persona updated." : "AI persona updated.", "success");
|
setNotice("User persona updated.", "success");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clearPersona(type) {
|
async function clearPersona(type) {
|
||||||
const res = await post(type === "user" ? "/personas/user/clear" : "/personas/clear", {});
|
if (type !== "user") return;
|
||||||
|
const res = await post("/personas/user/clear", {});
|
||||||
if (res.error) return setNotice(res.error, "error");
|
if (res.error) return setNotice(res.error, "error");
|
||||||
if (type === "user") activeUserPersonaId = null; else activePersonaId = null;
|
activeUserPersonaId = null;
|
||||||
renderPersonaList(assistantPersonas, activePersonaId, personaList, "assistant");
|
|
||||||
renderPersonaList(userPersonas, activeUserPersonaId, userPersonaList, "user");
|
renderPersonaList(userPersonas, activeUserPersonaId, userPersonaList, "user");
|
||||||
setNotice("Persona cleared.", "success");
|
setNotice("Persona cleared.", "success");
|
||||||
}
|
}
|
||||||
@@ -409,14 +512,16 @@ async function savePersona() {
|
|||||||
const res = editingPersonaId ? await put(`/personas/${editingPersonaId}`, payload) : await post("/personas", payload);
|
const res = editingPersonaId ? await put(`/personas/${editingPersonaId}`, payload) : await post("/personas", payload);
|
||||||
if (res.error) return setPersonaFormNotice(res.error, "error");
|
if (res.error) return setPersonaFormNotice(res.error, "error");
|
||||||
const wasEditing = Boolean(editingPersonaId);
|
const wasEditing = Boolean(editingPersonaId);
|
||||||
await loadPersonas(); closePersonaForm(); setNotice(wasEditing ? "Persona updated." : "Persona created.", "success");
|
await loadPersonas();
|
||||||
|
if (payload.personaType === "assistant") await loadChatSessions();
|
||||||
|
closePersonaForm(); setNotice(wasEditing ? "Persona updated." : "Persona created.", "success");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deletePersona(id) {
|
async function deletePersona(id) {
|
||||||
const persona = assistantPersonas.find((item) => item.id === id) || userPersonas.find((item) => item.id === id);
|
const persona = assistantPersonas.find((item) => item.id === id) || userPersonas.find((item) => item.id === id);
|
||||||
if (!persona) return;
|
if (!persona) return;
|
||||||
const confirmed = await confirmPopup({
|
const confirmed = await confirmPopup({
|
||||||
eyebrow: persona.persona_type === "user" ? "Your persona" : "AI persona",
|
eyebrow: persona.persona_type === "user" ? "Your persona" : "AI Character",
|
||||||
title: "Delete persona",
|
title: "Delete persona",
|
||||||
description: `Delete "${persona.name}"? This removes it from your library and any active slot.`,
|
description: `Delete "${persona.name}"? This removes it from your library and any active slot.`,
|
||||||
confirmLabel: "Delete",
|
confirmLabel: "Delete",
|
||||||
@@ -425,7 +530,9 @@ async function deletePersona(id) {
|
|||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
const res = await del(`/personas/${id}`);
|
const res = await del(`/personas/${id}`);
|
||||||
if (res.error) return setNotice(res.error, "error");
|
if (res.error) return setNotice(res.error, "error");
|
||||||
await loadPersonas(); setNotice("Persona deleted.", "success");
|
await loadPersonas();
|
||||||
|
if (persona.persona_type === "assistant") await loadChatSessions();
|
||||||
|
setNotice("Persona deleted.", "success");
|
||||||
}
|
}
|
||||||
async function publishPersona(id) { const res = await post(`/personas/${id}/publish`, {}); if (res.error) return setNotice(res.error, "error"); await loadPersonas(); setNotice("Persona published.", "success"); }
|
async function publishPersona(id) { const res = await post(`/personas/${id}/publish`, {}); if (res.error) return setNotice(res.error, "error"); await loadPersonas(); setNotice("Persona published.", "success"); }
|
||||||
async function unpublishPersona(id) { const res = await post(`/personas/${id}/unpublish`, {}); if (res.error) return setNotice(res.error, "error"); await loadPersonas(); setNotice("Persona unpublished.", "success"); }
|
async function unpublishPersona(id) { const res = await post(`/personas/${id}/unpublish`, {}); if (res.error) return setNotice(res.error, "error"); await loadPersonas(); setNotice("Persona unpublished.", "success"); }
|
||||||
@@ -446,6 +553,8 @@ async function setActiveChat(id) {
|
|||||||
updateWorkspaceCopy(); renderChatList(); updateChatActionState();
|
updateWorkspaceCopy(); renderChatList(); updateChatActionState();
|
||||||
const res = await get(`/chats/${id}/messages`);
|
const res = await get(`/chats/${id}/messages`);
|
||||||
if (res.error) return setNotice(res.error, "error");
|
if (res.error) return setNotice(res.error, "error");
|
||||||
|
const existing = getChatById(id);
|
||||||
|
if (res.chat && existing) Object.assign(existing, res.chat);
|
||||||
currentMessages = res.messages || []; renderMessages();
|
currentMessages = res.messages || []; renderMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,7 +562,10 @@ async function loadChatSessions() {
|
|||||||
const res = await get("/chats");
|
const res = await get("/chats");
|
||||||
if (res.error) return setNotice(res.error, "error");
|
if (res.error) return setNotice(res.error, "error");
|
||||||
chatSessions = res.chats || []; renderChatList();
|
chatSessions = res.chats || []; renderChatList();
|
||||||
if (chatSessions.length) return setActiveChat(activeChatId && getChatById(activeChatId) ? activeChatId : chatSessions[0].id);
|
if (chatSessions.length) {
|
||||||
|
const preferredChatId = (requestedChatId && getChatById(requestedChatId)) ? requestedChatId : activeChatId && getChatById(activeChatId) ? activeChatId : chatSessions[0].id;
|
||||||
|
return setActiveChat(preferredChatId);
|
||||||
|
}
|
||||||
return createNewChat();
|
return createNewChat();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,6 +575,29 @@ async function createNewChat() {
|
|||||||
chatSessions.unshift(res.chat); await loadSummary(); renderChatList(); await setActiveChat(res.chat.id); setNotice("New chat created.", "success");
|
chatSessions.unshift(res.chat); await loadSummary(); renderChatList(); await setActiveChat(res.chat.id); setNotice("New chat created.", "success");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function startRoleplay() {
|
||||||
|
const assistantPersonaId = Number(roleplayCharacterSelect.value);
|
||||||
|
const userPersonaId = roleplayUserPersonaSelect.value ? Number(roleplayUserPersonaSelect.value) : null;
|
||||||
|
if (!assistantPersonaId) {
|
||||||
|
return setRoleplayStarterNotice("Choose a character to start the roleplay.", "error");
|
||||||
|
}
|
||||||
|
roleplayStarterConfirm.disabled = true;
|
||||||
|
setRoleplayStarterNotice("Starting roleplay...");
|
||||||
|
const res = await post("/roleplays/start", {
|
||||||
|
assistantPersonaId,
|
||||||
|
userPersonaId,
|
||||||
|
model: modelSelect.value
|
||||||
|
});
|
||||||
|
roleplayStarterConfirm.disabled = false;
|
||||||
|
if (res.error || !res.chat) {
|
||||||
|
return setRoleplayStarterNotice(res.error || "Unable to start roleplay.", "error");
|
||||||
|
}
|
||||||
|
closeRoleplayStarter();
|
||||||
|
await loadChatSessions();
|
||||||
|
await setActiveChat(res.chat.id);
|
||||||
|
setNotice(res.generatedInitialMessage ? "Roleplay started." : "Roleplay reopened.", "success");
|
||||||
|
}
|
||||||
|
|
||||||
async function renameChat(id) {
|
async function renameChat(id) {
|
||||||
const chat = getChatById(id);
|
const chat = getChatById(id);
|
||||||
const nextTitle = chat ? await promptPopup({
|
const nextTitle = chat ? await promptPopup({
|
||||||
@@ -576,7 +711,7 @@ async function sendMessage() {
|
|||||||
if (!message) return;
|
if (!message) return;
|
||||||
if (!activeChatId) await createNewChat();
|
if (!activeChatId) await createNewChat();
|
||||||
const currentChat = getChatById(activeChatId);
|
const currentChat = getChatById(activeChatId);
|
||||||
if (currentChat && currentChat.title === "New chat") {
|
if (currentChat && !currentChat.assistant_persona_id && currentChat.title === "New chat") {
|
||||||
const autoTitle = message.length > 40 ? `${message.slice(0, 40)}...` : message;
|
const autoTitle = message.length > 40 ? `${message.slice(0, 40)}...` : message;
|
||||||
currentChat.title = autoTitle; updateWorkspaceCopy(); renderChatList(); await put(`/chats/${activeChatId}`, {title: autoTitle});
|
currentChat.title = autoTitle; updateWorkspaceCopy(); renderChatList(); await put(`/chats/${activeChatId}`, {title: autoTitle});
|
||||||
}
|
}
|
||||||
@@ -621,7 +756,7 @@ registerForm.addEventListener("submit", (event) => { event.preventDefault(); voi
|
|||||||
$("logout").addEventListener("click", async () => {
|
$("logout").addEventListener("click", async () => {
|
||||||
await post("/logout", {}); chatDiv.style.display = "none"; authDiv.style.display = "grid";
|
await post("/logout", {}); chatDiv.style.display = "none"; authDiv.style.display = "grid";
|
||||||
activeChatId = null; currentUsername = ""; currentSummary = null; chatSessions = []; currentMessages = []; assistantPersonas = []; userPersonas = [];
|
activeChatId = null; currentUsername = ""; currentSummary = null; chatSessions = []; currentMessages = []; assistantPersonas = []; userPersonas = [];
|
||||||
activePersonaId = null; activeUserPersonaId = null; publishedPersonaIds = new Set(); messagesDiv.innerHTML = ""; renderChatList(); updateChatActionState();
|
activeUserPersonaId = null; publishedPersonaIds = new Set(); messagesDiv.innerHTML = ""; renderChatList(); updateChatActionState();
|
||||||
showAuthScreen("login"); setAuthMessage("Logged out.", "success"); setNotice("Ready.");
|
showAuthScreen("login"); setAuthMessage("Logged out.", "success"); setNotice("Ready.");
|
||||||
});
|
});
|
||||||
sendBtn.addEventListener("click", () => { void sendMessage(); });
|
sendBtn.addEventListener("click", () => { void sendMessage(); });
|
||||||
@@ -631,8 +766,8 @@ newChatBtn.addEventListener("click", () => { void createNewChat(); });
|
|||||||
renameChatBtn.addEventListener("click", () => { if (activeChatId) void renameChat(activeChatId); });
|
renameChatBtn.addEventListener("click", () => { if (activeChatId) void renameChat(activeChatId); });
|
||||||
clearChatBtn.addEventListener("click", () => { if (activeChatId) void clearChat(activeChatId); }); exportChatBtn.addEventListener("click", exportCurrentChat);
|
clearChatBtn.addEventListener("click", () => { if (activeChatId) void clearChat(activeChatId); }); exportChatBtn.addEventListener("click", exportCurrentChat);
|
||||||
chatSearchInput.addEventListener("input", renderChatList); modelSelect.addEventListener("change", () => { updateModelHelp(); updateContextRail(); });
|
chatSearchInput.addEventListener("input", renderChatList); modelSelect.addEventListener("change", () => { updateModelHelp(); updateContextRail(); });
|
||||||
newPersonaBtn.addEventListener("click", () => openPersonaForm()); newUserPersonaBtn.addEventListener("click", () => openPersonaForm(null, "user"));
|
roleplayNewPersonaBtn.addEventListener("click", () => openPersonaForm(null, "assistant"));
|
||||||
clearPersonaBtn.addEventListener("click", () => { void clearPersona("assistant"); }); clearUserPersonaBtn.addEventListener("click", () => { void clearPersona("user"); });
|
clearUserPersonaBtn.addEventListener("click", () => { void clearPersona("user"); });
|
||||||
personaForm.addEventListener("submit", (event) => { event.preventDefault(); void savePersona(); }); personaCloseBtn.addEventListener("click", closePersonaForm);
|
personaForm.addEventListener("submit", (event) => { event.preventDefault(); void savePersona(); }); personaCloseBtn.addEventListener("click", closePersonaForm);
|
||||||
personaTypeSelect.addEventListener("change", () => {
|
personaTypeSelect.addEventListener("change", () => {
|
||||||
const isAssistant = personaTypeSelect.value === "assistant";
|
const isAssistant = personaTypeSelect.value === "assistant";
|
||||||
@@ -640,6 +775,10 @@ personaTypeSelect.addEventListener("change", () => {
|
|||||||
if (!isAssistant) personaExampleDialoguesInput.value = "";
|
if (!isAssistant) personaExampleDialoguesInput.value = "";
|
||||||
});
|
});
|
||||||
personaModal.addEventListener("click", (event) => { if (event.target === personaModal) closePersonaForm(); });
|
personaModal.addEventListener("click", (event) => { if (event.target === personaModal) closePersonaForm(); });
|
||||||
|
roleplayStarterClose.addEventListener("click", closeRoleplayStarter);
|
||||||
|
roleplayStarterCancel.addEventListener("click", closeRoleplayStarter);
|
||||||
|
roleplayStarterConfirm.addEventListener("click", () => { void startRoleplay(); });
|
||||||
|
roleplayStarterModal.addEventListener("click", (event) => { if (event.target === roleplayStarterModal) closeRoleplayStarter(); });
|
||||||
popupConfirmBtn.addEventListener("click", () => {
|
popupConfirmBtn.addEventListener("click", () => {
|
||||||
if (popupMode === "prompt") {
|
if (popupMode === "prompt") {
|
||||||
closePopup(popupInput.value);
|
closePopup(popupInput.value);
|
||||||
@@ -663,6 +802,7 @@ document.addEventListener("keydown", (event) => {
|
|||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
closeModelPopover();
|
closeModelPopover();
|
||||||
closePersonaPopover();
|
closePersonaPopover();
|
||||||
|
closeRoleplayStarter();
|
||||||
closePersonaForm();
|
closePersonaForm();
|
||||||
closePopup(false);
|
closePopup(false);
|
||||||
}
|
}
|
||||||
|
|||||||
+92
-2
@@ -15,11 +15,11 @@
|
|||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Settings</p>
|
<p class="eyebrow">Settings</p>
|
||||||
<h1>Preferences</h1>
|
<h1>Preferences</h1>
|
||||||
<p class="subtitle">Manage account details and appearance.</p>
|
<p class="subtitle">Manage your account, AI Characters, user personas, and appearance.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="market-header-actions">
|
<div class="market-header-actions">
|
||||||
<a href="/" class="secondary-action">Back to chat</a>
|
<a href="/" class="secondary-action">Back to chat</a>
|
||||||
<a href="/market" class="secondary-action">Persona Market</a>
|
<a href="/market" class="secondary-action">Market</a>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -28,6 +28,8 @@
|
|||||||
<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>
|
||||||
|
<button class="settings-nav-item" type="button" data-settings-view="my-ai-characters">My AI Characters</button>
|
||||||
|
<button class="settings-nav-item" type="button" data-settings-view="my-personas">My Personas</button>
|
||||||
<button class="settings-nav-item" type="button" data-settings-view="design">Design</button>
|
<button class="settings-nav-item" type="button" data-settings-view="design">Design</button>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
@@ -82,6 +84,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="settings-view" data-settings-panel="my-ai-characters">
|
||||||
|
<div class="settings-panel">
|
||||||
|
<div class="settings-section-header">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Manage</p>
|
||||||
|
<h2>My AI Characters</h2>
|
||||||
|
<p class="subtitle">Create, edit, publish, and delete your AI Characters.</p>
|
||||||
|
</div>
|
||||||
|
<button id="createAiCharacter" type="button" class="secondary-action">Create persona</button>
|
||||||
|
</div>
|
||||||
|
<div id="settingsAiCharacterList" class="persona-list"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="settings-view" data-settings-panel="my-personas">
|
||||||
|
<div class="settings-panel">
|
||||||
|
<div class="settings-section-header">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Manage</p>
|
||||||
|
<h2>My Personas</h2>
|
||||||
|
<p class="subtitle">Create and manage the user personas you can speak as in roleplay chats.</p>
|
||||||
|
</div>
|
||||||
|
<button id="createUserPersona" type="button" class="secondary-action">Create persona</button>
|
||||||
|
</div>
|
||||||
|
<div id="settingsUserPersonaList" class="persona-list"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="settings-view" data-settings-panel="design">
|
<section class="settings-view" data-settings-panel="design">
|
||||||
<div class="settings-panel settings-panel-compact">
|
<div class="settings-panel settings-panel-compact">
|
||||||
<div class="settings-section-header">
|
<div class="settings-section-header">
|
||||||
@@ -109,6 +139,66 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="settingsPersonaModal" class="modal-overlay hidden">
|
||||||
|
<div class="modal-card persona-modal-card">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="persona-modal-heading">
|
||||||
|
<p class="eyebrow">Persona editor</p>
|
||||||
|
<h3 id="settingsPersonaFormTitle">Create persona</h3>
|
||||||
|
</div>
|
||||||
|
<button id="settingsPersonaClose" type="button" class="icon-button">Close</button>
|
||||||
|
</div>
|
||||||
|
<form id="settingsPersonaForm" class="persona-form">
|
||||||
|
<div class="persona-form-intro">
|
||||||
|
<p id="settingsPersonaHelper" class="persona-helper">Create a persona for roleplay chats.</p>
|
||||||
|
</div>
|
||||||
|
<p id="settingsPersonaFormNotice" class="status hidden"></p>
|
||||||
|
<div class="persona-form-grid">
|
||||||
|
<label class="field field-wide">
|
||||||
|
<span>Persona for</span>
|
||||||
|
<select id="settingsPersonaType">
|
||||||
|
<option value="assistant">AI Character</option>
|
||||||
|
<option value="user">Your persona</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>Name</span>
|
||||||
|
<input id="settingsPersonaName" placeholder="Persona name" required/>
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>Pronouns</span>
|
||||||
|
<input id="settingsPersonaPronouns" placeholder="e.g. she/her, they/them"/>
|
||||||
|
</label>
|
||||||
|
<label class="field field-wide">
|
||||||
|
<span>Appearance</span>
|
||||||
|
<textarea id="settingsPersonaAppearance" rows="3" placeholder="Describe their look"></textarea>
|
||||||
|
</label>
|
||||||
|
<label class="field field-wide">
|
||||||
|
<span>Background</span>
|
||||||
|
<textarea id="settingsPersonaBackground" rows="3" placeholder="Backstory, history, role, or origins"></textarea>
|
||||||
|
</label>
|
||||||
|
<label class="field field-wide">
|
||||||
|
<span>Details</span>
|
||||||
|
<textarea id="settingsPersonaDetails" rows="4" placeholder="Personality traits, voice, goals, quirks, boundaries, style"></textarea>
|
||||||
|
</label>
|
||||||
|
<label id="settingsPersonaExamplesField" class="field field-wide hidden">
|
||||||
|
<span>Example dialogues</span>
|
||||||
|
<div class="dialogue-format-card" aria-hidden="true">
|
||||||
|
<p class="dialogue-format-label">Format</p>
|
||||||
|
<pre class="dialogue-format-example">{{character}}: Hello, my name is marc
|
||||||
|
{{user}}: Hi, Marc!
|
||||||
|
{{character}}: Nice to meet you</pre>
|
||||||
|
</div>
|
||||||
|
<textarea id="settingsPersonaExampleDialogues" rows="5" placeholder="Write example exchanges that define the character's tone, phrasing, pacing, and personality."></textarea>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="persona-actions">
|
||||||
|
<button id="settingsPersonaSave" type="submit" class="primary-action">Save persona</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<script src="settings.js" defer></script>
|
<script src="settings.js" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+243
-12
@@ -1,7 +1,6 @@
|
|||||||
const $ = (id) => document.getElementById(id);
|
const $ = (id) => document.getElementById(id);
|
||||||
const settingsNotice = $("settingsNotice");
|
const settingsNotice = $("settingsNotice");
|
||||||
const settingsThemeSelect = $("settingsThemeSelect");
|
const settingsThemeSelect = $("settingsThemeSelect");
|
||||||
const settingsThemeName = $("settingsThemeName");
|
|
||||||
const currentUsernameInput = $("currentUsername");
|
const currentUsernameInput = $("currentUsername");
|
||||||
const newUsernameInput = $("newUsername");
|
const newUsernameInput = $("newUsername");
|
||||||
const usernamePasswordInput = $("usernamePassword");
|
const usernamePasswordInput = $("usernamePassword");
|
||||||
@@ -12,6 +11,24 @@ const usernameForm = $("usernameForm");
|
|||||||
const passwordForm = $("passwordForm");
|
const passwordForm = $("passwordForm");
|
||||||
const settingsNavItems = document.querySelectorAll("[data-settings-view]");
|
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 createUserPersonaBtn = $("createUserPersona");
|
||||||
|
const settingsAiCharacterList = $("settingsAiCharacterList");
|
||||||
|
const settingsUserPersonaList = $("settingsUserPersonaList");
|
||||||
|
const settingsPersonaModal = $("settingsPersonaModal");
|
||||||
|
const settingsPersonaClose = $("settingsPersonaClose");
|
||||||
|
const settingsPersonaForm = $("settingsPersonaForm");
|
||||||
|
const settingsPersonaFormTitle = $("settingsPersonaFormTitle");
|
||||||
|
const settingsPersonaFormNotice = $("settingsPersonaFormNotice");
|
||||||
|
const settingsPersonaHelper = $("settingsPersonaHelper");
|
||||||
|
const settingsPersonaType = $("settingsPersonaType");
|
||||||
|
const settingsPersonaName = $("settingsPersonaName");
|
||||||
|
const settingsPersonaPronouns = $("settingsPersonaPronouns");
|
||||||
|
const settingsPersonaAppearance = $("settingsPersonaAppearance");
|
||||||
|
const settingsPersonaBackground = $("settingsPersonaBackground");
|
||||||
|
const settingsPersonaDetails = $("settingsPersonaDetails");
|
||||||
|
const settingsPersonaExamplesField = $("settingsPersonaExamplesField");
|
||||||
|
const settingsPersonaExampleDialogues = $("settingsPersonaExampleDialogues");
|
||||||
|
|
||||||
const themes = {
|
const themes = {
|
||||||
"fakegpt": {name: "FakeGPT"},
|
"fakegpt": {name: "FakeGPT"},
|
||||||
@@ -22,6 +39,11 @@ const themes = {
|
|||||||
"confusity": {name: "Confusity"}
|
"confusity": {name: "Confusity"}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let assistantPersonas = [];
|
||||||
|
let userPersonas = [];
|
||||||
|
let publishedPersonaIds = new Set();
|
||||||
|
let editingPersonaId = null;
|
||||||
|
|
||||||
async function request(url, data, method = "POST") {
|
async function request(url, data, method = "POST") {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
@@ -35,34 +57,220 @@ async function request(url, data, method = "POST") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const get = (url) => request(url, null, "GET");
|
||||||
|
const post = (url, data) => request(url, data, "POST");
|
||||||
|
const put = (url, data) => request(url, data, "PUT");
|
||||||
|
const del = (url) => request(url, null, "DELETE");
|
||||||
|
|
||||||
function setNotice(message, state = "") {
|
function setNotice(message, state = "") {
|
||||||
settingsNotice.textContent = message;
|
settingsNotice.textContent = message;
|
||||||
settingsNotice.className = state ? `status ${state}` : "status";
|
settingsNotice.className = state ? `status ${state}` : "status";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setPersonaNotice(message = "", state = "") {
|
||||||
|
if (!message) {
|
||||||
|
settingsPersonaFormNotice.textContent = "";
|
||||||
|
settingsPersonaFormNotice.className = "status hidden";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
settingsPersonaFormNotice.textContent = message;
|
||||||
|
settingsPersonaFormNotice.className = state ? `status ${state}` : "status";
|
||||||
|
}
|
||||||
|
|
||||||
function applyTheme(themeKey, persist = true) {
|
function applyTheme(themeKey, persist = true) {
|
||||||
const nextTheme = themes[themeKey] ? themeKey : "fakegpt";
|
const nextTheme = themes[themeKey] ? themeKey : "fakegpt";
|
||||||
document.body.dataset.theme = nextTheme;
|
document.body.dataset.theme = nextTheme;
|
||||||
document.title = "Settings";
|
document.title = "Settings";
|
||||||
settingsThemeSelect.value = nextTheme;
|
settingsThemeSelect.value = nextTheme;
|
||||||
if (settingsThemeName) settingsThemeName.textContent = themes[nextTheme].name;
|
|
||||||
if (persist) localStorage.setItem("krishd-theme", nextTheme);
|
if (persist) localStorage.setItem("krishd-theme", nextTheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setSettingsView(view) {
|
function setSettingsView(view) {
|
||||||
settingsNavItems.forEach((item) => item.classList.toggle("active", item.dataset.settingsView === view));
|
settingsNavItems.forEach((item) => item.classList.toggle("active", item.dataset.settingsView === view));
|
||||||
settingsPanels.forEach((panel) => panel.classList.toggle("active", panel.dataset.settingsPanel === view));
|
settingsPanels.forEach((panel) => panel.classList.toggle("active", panel.dataset.settingsPanel === view));
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
params.set("view", view);
|
||||||
|
history.replaceState(null, "", `${window.location.pathname}?${params.toString()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMeta(persona) {
|
||||||
|
return persona.pronouns ? `Pronouns: ${persona.pronouns}` : "Pronouns: n/a";
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncPersonaExamplesField() {
|
||||||
|
const isAssistant = settingsPersonaType.value === "assistant";
|
||||||
|
settingsPersonaExamplesField.classList.toggle("hidden", !isAssistant);
|
||||||
|
settingsPersonaHelper.textContent = isAssistant
|
||||||
|
? "AI Characters define who the character is, how they speak, and what kind of scene they lead."
|
||||||
|
: "User personas define who you are when starting roleplay chats with AI Characters.";
|
||||||
|
$("settingsPersonaSave").textContent = isAssistant ? "Save AI Character" : "Save persona";
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPersonaModal(persona = null, personaType = "assistant") {
|
||||||
|
editingPersonaId = persona ? persona.id : null;
|
||||||
|
settingsPersonaType.disabled = Boolean(persona);
|
||||||
|
settingsPersonaType.value = persona ? persona.persona_type : personaType;
|
||||||
|
settingsPersonaFormTitle.textContent = persona
|
||||||
|
? `Edit ${settingsPersonaType.value === "assistant" ? "AI Character" : "persona"}`
|
||||||
|
: `Create ${personaType === "assistant" ? "AI Character" : "persona"}`;
|
||||||
|
settingsPersonaName.value = persona?.name || "";
|
||||||
|
settingsPersonaPronouns.value = persona?.pronouns || "";
|
||||||
|
settingsPersonaAppearance.value = persona?.appearance || "";
|
||||||
|
settingsPersonaBackground.value = persona?.background || "";
|
||||||
|
settingsPersonaDetails.value = persona?.details || "";
|
||||||
|
settingsPersonaExampleDialogues.value = persona?.example_dialogues || "";
|
||||||
|
syncPersonaExamplesField();
|
||||||
|
setPersonaNotice("");
|
||||||
|
settingsPersonaModal.classList.remove("hidden");
|
||||||
|
settingsPersonaName.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePersonaModal() {
|
||||||
|
settingsPersonaModal.classList.add("hidden");
|
||||||
|
editingPersonaId = null;
|
||||||
|
setPersonaNotice("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPersonaList(personas, listElement, type) {
|
||||||
|
listElement.innerHTML = "";
|
||||||
|
if (!personas.length) {
|
||||||
|
const empty = document.createElement("p");
|
||||||
|
empty.className = "status persona-status";
|
||||||
|
empty.textContent = type === "assistant"
|
||||||
|
? "No AI Characters yet. Create one to get started."
|
||||||
|
: "No user personas yet. Create one to talk as someone specific.";
|
||||||
|
listElement.appendChild(empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
personas.forEach((persona) => {
|
||||||
|
const item = document.createElement("div");
|
||||||
|
const titleRow = document.createElement("div");
|
||||||
|
const title = document.createElement("h4");
|
||||||
|
const tag = document.createElement("span");
|
||||||
|
const meta = document.createElement("p");
|
||||||
|
const actions = document.createElement("div");
|
||||||
|
|
||||||
|
item.className = "persona-item";
|
||||||
|
titleRow.className = "persona-title-row";
|
||||||
|
actions.className = "persona-item-actions";
|
||||||
|
tag.className = `persona-role-tag ${type === "assistant" ? "ai" : "user"}`;
|
||||||
|
tag.textContent = type === "assistant" ? "AI Character" : "User";
|
||||||
|
title.textContent = persona.name;
|
||||||
|
meta.textContent = formatMeta(persona);
|
||||||
|
|
||||||
|
[
|
||||||
|
["Edit", () => openPersonaModal(persona)],
|
||||||
|
[type === "assistant" && publishedPersonaIds.has(persona.id) ? "Update listing" : type === "assistant" ? "Publish" : null, () => publishPersona(persona.id)],
|
||||||
|
["Delete", () => deletePersona(persona.id)]
|
||||||
|
]
|
||||||
|
.filter(([label]) => Boolean(label))
|
||||||
|
.forEach(([label, handler]) => {
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.type = "button";
|
||||||
|
button.textContent = label;
|
||||||
|
button.addEventListener("click", handler);
|
||||||
|
actions.appendChild(button);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (type === "assistant" && publishedPersonaIds.has(persona.id)) {
|
||||||
|
const unpublishButton = document.createElement("button");
|
||||||
|
unpublishButton.type = "button";
|
||||||
|
unpublishButton.textContent = "Unpublish";
|
||||||
|
unpublishButton.addEventListener("click", () => unpublishPersona(persona.id));
|
||||||
|
actions.appendChild(unpublishButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
titleRow.append(title, tag);
|
||||||
|
item.append(titleRow, meta, actions);
|
||||||
|
listElement.appendChild(item);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadProfile() {
|
async function loadProfile() {
|
||||||
const res = await request("/settings/profile", null, "GET");
|
const res = await get("/settings/profile");
|
||||||
|
if (res.error) {
|
||||||
|
setNotice(res.error, "error");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
currentUsernameInput.value = res.username || "";
|
||||||
|
newUsernameInput.value = res.username || "";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPersonas() {
|
||||||
|
const res = await get("/personas");
|
||||||
|
if (res.error) {
|
||||||
|
setNotice(res.error, "error");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
assistantPersonas = res.assistantPersonas || [];
|
||||||
|
userPersonas = res.userPersonas || [];
|
||||||
|
publishedPersonaIds = new Set(res.publishedPersonaIds || []);
|
||||||
|
renderPersonaList(assistantPersonas, settingsAiCharacterList, "assistant");
|
||||||
|
renderPersonaList(userPersonas, settingsUserPersonaList, "user");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function savePersona() {
|
||||||
|
const payload = {
|
||||||
|
personaType: settingsPersonaType.value,
|
||||||
|
name: settingsPersonaName.value.trim(),
|
||||||
|
pronouns: settingsPersonaPronouns.value.trim(),
|
||||||
|
appearance: settingsPersonaAppearance.value.trim(),
|
||||||
|
background: settingsPersonaBackground.value.trim(),
|
||||||
|
details: settingsPersonaDetails.value.trim(),
|
||||||
|
exampleDialogues: settingsPersonaType.value === "assistant" ? settingsPersonaExampleDialogues.value.trim() : ""
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!payload.name) {
|
||||||
|
setPersonaNotice("Name is required.", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = editingPersonaId
|
||||||
|
? await put(`/personas/${editingPersonaId}`, payload)
|
||||||
|
: await post("/personas", payload);
|
||||||
|
|
||||||
|
if (res.error) {
|
||||||
|
setPersonaNotice(res.error, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadPersonas();
|
||||||
|
closePersonaModal();
|
||||||
|
setSettingsView(payload.personaType === "assistant" ? "my-ai-characters" : "my-personas");
|
||||||
|
setNotice(editingPersonaId ? "Persona updated." : "Persona created.", "success");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deletePersona(id) {
|
||||||
|
const res = await del(`/personas/${id}`);
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
setNotice(res.error, "error");
|
setNotice(res.error, "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
currentUsernameInput.value = res.username || "";
|
await loadPersonas();
|
||||||
newUsernameInput.value = res.username || "";
|
setNotice("Persona deleted.", "success");
|
||||||
setNotice("Settings loaded.");
|
}
|
||||||
|
|
||||||
|
async function publishPersona(id) {
|
||||||
|
const res = await post(`/personas/${id}/publish`, {});
|
||||||
|
if (res.error) {
|
||||||
|
setNotice(res.error, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await loadPersonas();
|
||||||
|
setNotice("AI Character published.", "success");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unpublishPersona(id) {
|
||||||
|
const res = await post(`/personas/${id}/unpublish`, {});
|
||||||
|
if (res.error) {
|
||||||
|
setNotice(res.error, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await loadPersonas();
|
||||||
|
setNotice("AI Character unpublished.", "success");
|
||||||
}
|
}
|
||||||
|
|
||||||
usernameForm.addEventListener("submit", async (event) => {
|
usernameForm.addEventListener("submit", async (event) => {
|
||||||
@@ -87,17 +295,17 @@ usernameForm.addEventListener("submit", async (event) => {
|
|||||||
passwordForm.addEventListener("submit", async (event) => {
|
passwordForm.addEventListener("submit", async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const currentPassword = currentPasswordInput.value;
|
const currentPassword = currentPasswordInput.value;
|
||||||
const newPassword = newPasswordInput.value;
|
const nextPassword = newPasswordInput.value;
|
||||||
const repeatPassword = repeatPasswordInput.value;
|
const repeatPassword = repeatPasswordInput.value;
|
||||||
if (!currentPassword || !newPassword || !repeatPassword) {
|
if (!currentPassword || !nextPassword || !repeatPassword) {
|
||||||
setNotice("Fill in all password fields.", "error");
|
setNotice("Fill in all password fields.", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (newPassword !== repeatPassword) {
|
if (nextPassword !== repeatPassword) {
|
||||||
setNotice("New passwords do not match.", "error");
|
setNotice("New passwords do not match.", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const res = await request("/settings/password", {currentPassword, newPassword}, "PUT");
|
const res = await request("/settings/password", {currentPassword, newPassword: nextPassword}, "PUT");
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
setNotice(res.error, "error");
|
setNotice(res.error, "error");
|
||||||
return;
|
return;
|
||||||
@@ -114,9 +322,32 @@ settingsThemeSelect.addEventListener("change", (event) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
settingsNavItems.forEach((item) => item.addEventListener("click", () => setSettingsView(item.dataset.settingsView)));
|
settingsNavItems.forEach((item) => item.addEventListener("click", () => setSettingsView(item.dataset.settingsView)));
|
||||||
|
createAiCharacterBtn.addEventListener("click", () => openPersonaModal(null, "assistant"));
|
||||||
|
createUserPersonaBtn.addEventListener("click", () => openPersonaModal(null, "user"));
|
||||||
|
settingsPersonaClose.addEventListener("click", closePersonaModal);
|
||||||
|
settingsPersonaModal.addEventListener("click", (event) => {
|
||||||
|
if (event.target === settingsPersonaModal) closePersonaModal();
|
||||||
|
});
|
||||||
|
settingsPersonaType.addEventListener("change", syncPersonaExamplesField);
|
||||||
|
settingsPersonaForm.addEventListener("submit", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
void savePersona();
|
||||||
|
});
|
||||||
|
|
||||||
window.addEventListener("load", async () => {
|
window.addEventListener("load", async () => {
|
||||||
applyTheme(localStorage.getItem("krishd-theme") || "fakegpt", false);
|
applyTheme(localStorage.getItem("krishd-theme") || "fakegpt", false);
|
||||||
setSettingsView("personal");
|
const params = new URLSearchParams(window.location.search);
|
||||||
await loadProfile();
|
const view = params.get("view") || "personal";
|
||||||
|
setSettingsView(view);
|
||||||
|
const [profileOk, personasOk] = await Promise.all([loadProfile(), loadPersonas()]);
|
||||||
|
if (profileOk && personasOk) setNotice("Settings loaded.");
|
||||||
|
|
||||||
|
const create = params.get("create");
|
||||||
|
if (create === "assistant") {
|
||||||
|
setSettingsView("my-ai-characters");
|
||||||
|
openPersonaModal(null, "assistant");
|
||||||
|
} else if (create === "user") {
|
||||||
|
setSettingsView("my-personas");
|
||||||
|
openPersonaModal(null, "user");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+70
-1
@@ -384,6 +384,10 @@ h2 {
|
|||||||
font-family: "Space Grotesk", sans-serif;
|
font-family: "Space Grotesk", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-mode-pill {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
.auth-toggle,
|
.auth-toggle,
|
||||||
.persona-form,
|
.persona-form,
|
||||||
.input-shell,
|
.input-shell,
|
||||||
@@ -589,6 +593,7 @@ select:focus {
|
|||||||
.chat-title-block,
|
.chat-title-block,
|
||||||
.chat-toolbar,
|
.chat-toolbar,
|
||||||
.chat-header-actions,
|
.chat-header-actions,
|
||||||
|
.sidebar-header-actions,
|
||||||
.market-header,
|
.market-header,
|
||||||
.market-header-actions,
|
.market-header-actions,
|
||||||
.persona-title-row,
|
.persona-title-row,
|
||||||
@@ -663,6 +668,19 @@ select:focus {
|
|||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-item-copy {
|
||||||
|
min-width: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-item-meta {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.76rem;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-item button,
|
.chat-item button,
|
||||||
.icon-button {
|
.icon-button {
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
@@ -792,6 +810,18 @@ select:focus {
|
|||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-participants {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header-actions {
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-toolbar {
|
.chat-toolbar {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
@@ -1259,7 +1289,7 @@ select:focus {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(100% + 10px);
|
top: calc(100% + 10px);
|
||||||
right: 0;
|
right: 0;
|
||||||
width: min(560px, 88vw);
|
width: min(460px, 88vw);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
@@ -1273,6 +1303,40 @@ select:focus {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.character-library-card {
|
||||||
|
width: min(760px, 95vw);
|
||||||
|
display: grid;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-library-intro {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 16px 18px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: rgba(24, 32, 31, 0.04);
|
||||||
|
border: 1px solid rgba(24, 32, 31, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-library-list {
|
||||||
|
max-height: min(56vh, 520px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.roleplay-starter-card {
|
||||||
|
width: min(620px, 95vw);
|
||||||
|
display: grid;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roleplay-starter-intro {
|
||||||
|
padding: 16px 18px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: rgba(24, 32, 31, 0.04);
|
||||||
|
border: 1px solid rgba(24, 32, 31, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
.persona-status {
|
.persona-status {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
@@ -1310,6 +1374,10 @@ select:focus {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.market-preview-user-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.persona-item-actions button {
|
.persona-item-actions button {
|
||||||
padding: 8px 11px;
|
padding: 8px 11px;
|
||||||
font-size: 0.84rem;
|
font-size: 0.84rem;
|
||||||
@@ -1515,6 +1583,7 @@ select:focus {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-shell {
|
.settings-shell {
|
||||||
|
|||||||
@@ -131,6 +131,10 @@ db.prepare(`
|
|||||||
TEXT
|
TEXT
|
||||||
NOT
|
NOT
|
||||||
NULL,
|
NULL,
|
||||||
|
assistant_persona_id
|
||||||
|
INTEGER,
|
||||||
|
user_persona_id
|
||||||
|
INTEGER,
|
||||||
created_at
|
created_at
|
||||||
DATETIME
|
DATETIME
|
||||||
DEFAULT
|
DEFAULT
|
||||||
@@ -138,7 +142,23 @@ db.prepare(`
|
|||||||
updated_at
|
updated_at
|
||||||
DATETIME
|
DATETIME
|
||||||
DEFAULT
|
DEFAULT
|
||||||
CURRENT_TIMESTAMP
|
CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN
|
||||||
|
KEY
|
||||||
|
(
|
||||||
|
assistant_persona_id
|
||||||
|
) REFERENCES personas
|
||||||
|
(
|
||||||
|
id
|
||||||
|
) ON DELETE SET NULL,
|
||||||
|
FOREIGN
|
||||||
|
KEY
|
||||||
|
(
|
||||||
|
user_persona_id
|
||||||
|
) REFERENCES personas
|
||||||
|
(
|
||||||
|
id
|
||||||
|
) ON DELETE SET NULL
|
||||||
)
|
)
|
||||||
`).run();
|
`).run();
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
@@ -311,13 +331,26 @@ 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) VALUES (?, ?)"
|
"INSERT INTO chat_sessions (username, title, assistant_persona_id, user_persona_id) VALUES (?, ?, ?, ?)"
|
||||||
);
|
);
|
||||||
const listChatSessionsStmt = db.prepare(`
|
const listChatSessionsStmt = db.prepare(`
|
||||||
SELECT id, title, created_at, updated_at
|
SELECT cs.id,
|
||||||
FROM chat_sessions
|
CASE
|
||||||
WHERE username = ?
|
WHEN cs.assistant_persona_id IS NOT NULL THEN COALESCE(p.name, cs.title)
|
||||||
ORDER BY datetime(updated_at) DESC
|
ELSE cs.title
|
||||||
|
END AS title,
|
||||||
|
cs.title AS stored_title,
|
||||||
|
cs.assistant_persona_id,
|
||||||
|
p.name AS assistant_persona_name,
|
||||||
|
cs.user_persona_id,
|
||||||
|
up.name AS user_persona_name,
|
||||||
|
cs.created_at,
|
||||||
|
cs.updated_at
|
||||||
|
FROM chat_sessions cs
|
||||||
|
LEFT JOIN personas p ON p.id = cs.assistant_persona_id
|
||||||
|
LEFT JOIN personas up ON up.id = cs.user_persona_id
|
||||||
|
WHERE cs.username = ?
|
||||||
|
ORDER BY datetime(cs.updated_at) DESC
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const countChatSessionsStmt = db.prepare(`
|
const countChatSessionsStmt = db.prepare(`
|
||||||
@@ -326,9 +359,47 @@ const countChatSessionsStmt = db.prepare(`
|
|||||||
WHERE username = ?
|
WHERE username = ?
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const getChatSessionStmt = db.prepare(
|
const getChatSessionStmt = db.prepare(`
|
||||||
"SELECT id, title FROM chat_sessions WHERE id = ? AND username = ?"
|
SELECT cs.id,
|
||||||
);
|
CASE
|
||||||
|
WHEN cs.assistant_persona_id IS NOT NULL THEN COALESCE(p.name, cs.title)
|
||||||
|
ELSE cs.title
|
||||||
|
END AS title,
|
||||||
|
cs.title AS stored_title,
|
||||||
|
cs.assistant_persona_id,
|
||||||
|
p.name AS assistant_persona_name,
|
||||||
|
cs.user_persona_id,
|
||||||
|
up.name AS user_persona_name
|
||||||
|
FROM chat_sessions cs
|
||||||
|
LEFT JOIN personas p ON p.id = cs.assistant_persona_id
|
||||||
|
LEFT JOIN personas up ON up.id = cs.user_persona_id
|
||||||
|
WHERE cs.id = ?
|
||||||
|
AND cs.username = ?
|
||||||
|
`);
|
||||||
|
|
||||||
|
const getChatSessionByParticipantsStmt = db.prepare(`
|
||||||
|
SELECT cs.id,
|
||||||
|
CASE
|
||||||
|
WHEN cs.assistant_persona_id IS NOT NULL THEN COALESCE(p.name, cs.title)
|
||||||
|
ELSE cs.title
|
||||||
|
END AS title,
|
||||||
|
cs.title AS stored_title,
|
||||||
|
cs.assistant_persona_id,
|
||||||
|
p.name AS assistant_persona_name,
|
||||||
|
cs.user_persona_id,
|
||||||
|
up.name AS user_persona_name
|
||||||
|
FROM chat_sessions cs
|
||||||
|
LEFT JOIN personas p ON p.id = cs.assistant_persona_id
|
||||||
|
LEFT JOIN personas up ON up.id = cs.user_persona_id
|
||||||
|
WHERE cs.username = ?
|
||||||
|
AND cs.assistant_persona_id = ?
|
||||||
|
AND (
|
||||||
|
(cs.user_persona_id IS NULL AND ? IS NULL)
|
||||||
|
OR cs.user_persona_id = ?
|
||||||
|
)
|
||||||
|
ORDER BY datetime(cs.updated_at) DESC
|
||||||
|
LIMIT 1
|
||||||
|
`);
|
||||||
|
|
||||||
const updateChatSessionTitleStmt = db.prepare(
|
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 = ?"
|
||||||
@@ -342,6 +413,22 @@ const deleteChatSessionStmt = db.prepare(
|
|||||||
"DELETE FROM chat_sessions WHERE id = ? AND username = ?"
|
"DELETE FROM chat_sessions WHERE id = ? AND username = ?"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const clearAssistantPersonaFromChatsStmt = db.prepare(`
|
||||||
|
UPDATE chat_sessions
|
||||||
|
SET assistant_persona_id = NULL,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE username = ?
|
||||||
|
AND assistant_persona_id = ?
|
||||||
|
`);
|
||||||
|
|
||||||
|
const clearUserPersonaFromChatsStmt = db.prepare(`
|
||||||
|
UPDATE chat_sessions
|
||||||
|
SET user_persona_id = NULL,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE username = ?
|
||||||
|
AND user_persona_id = ?
|
||||||
|
`);
|
||||||
|
|
||||||
const insertChatMessageStmt = db.prepare(
|
const insertChatMessageStmt = db.prepare(
|
||||||
"INSERT INTO chat_messages (chat_id, role, content) VALUES (?, ?, ?)"
|
"INSERT INTO chat_messages (chat_id, role, content) VALUES (?, ?, ?)"
|
||||||
);
|
);
|
||||||
@@ -472,6 +559,22 @@ const getActiveUserPersonaStmt = db.prepare(`
|
|||||||
WHERE us.username = ?
|
WHERE us.username = ?
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
const getAssistantPersonaForChatStmt = db.prepare(`
|
||||||
|
SELECT p.id, p.name, p.pronouns, p.appearance, p.background, p.details, p.example_dialogues
|
||||||
|
FROM chat_sessions cs
|
||||||
|
JOIN personas p ON p.id = cs.assistant_persona_id
|
||||||
|
WHERE cs.id = ?
|
||||||
|
AND cs.username = ?
|
||||||
|
`);
|
||||||
|
|
||||||
|
const getUserPersonaForChatStmt = db.prepare(`
|
||||||
|
SELECT p.id, p.name, p.pronouns, p.appearance, p.background, p.details, p.example_dialogues
|
||||||
|
FROM chat_sessions cs
|
||||||
|
JOIN personas p ON p.id = cs.user_persona_id
|
||||||
|
WHERE cs.id = ?
|
||||||
|
AND cs.username = ?
|
||||||
|
`);
|
||||||
|
|
||||||
const listMarketPersonasStmt = db.prepare(`
|
const listMarketPersonasStmt = db.prepare(`
|
||||||
SELECT id,
|
SELECT id,
|
||||||
persona_id,
|
persona_id,
|
||||||
@@ -630,7 +733,7 @@ if (legacyChatsExist && migrationNeeded.count === 0) {
|
|||||||
"SELECT DISTINCT username FROM chats"
|
"SELECT DISTINCT username FROM chats"
|
||||||
).all();
|
).all();
|
||||||
legacyUsers.forEach(({username}) => {
|
legacyUsers.forEach(({username}) => {
|
||||||
const sessionId = insertChatSessionStmt.run(username, "Imported chat").lastInsertRowid;
|
const sessionId = insertChatSessionStmt.run(username, "Imported chat", null, null).lastInsertRowid;
|
||||||
const legacyMessages = getLegacyChatsStmt.all(username);
|
const legacyMessages = getLegacyChatsStmt.all(username);
|
||||||
legacyMessages.forEach(msg => {
|
legacyMessages.forEach(msg => {
|
||||||
insertChatMessageStmt.run(sessionId, msg.role, msg.content);
|
insertChatMessageStmt.run(sessionId, msg.role, msg.content);
|
||||||
@@ -681,6 +784,16 @@ if (!hasMarketExampleDialogues) {
|
|||||||
}
|
}
|
||||||
db.prepare("UPDATE persona_market SET usage_count = 0 WHERE usage_count IS NULL").run();
|
db.prepare("UPDATE persona_market SET usage_count = 0 WHERE usage_count IS NULL").run();
|
||||||
|
|
||||||
|
const chatSessionColumns = db.prepare("PRAGMA table_info(chat_sessions)").all();
|
||||||
|
const hasAssistantPersonaLink = chatSessionColumns.some(column => column.name === "assistant_persona_id");
|
||||||
|
if (!hasAssistantPersonaLink) {
|
||||||
|
db.prepare("ALTER TABLE chat_sessions ADD COLUMN assistant_persona_id INTEGER").run();
|
||||||
|
}
|
||||||
|
const hasChatUserPersonaLink = chatSessionColumns.some(column => column.name === "user_persona_id");
|
||||||
|
if (!hasChatUserPersonaLink) {
|
||||||
|
db.prepare("ALTER TABLE chat_sessions ADD COLUMN user_persona_id INTEGER").run();
|
||||||
|
}
|
||||||
|
|
||||||
const FileStore = FileStoreFactory(session);
|
const FileStore = FileStoreFactory(session);
|
||||||
app.use(bodyParser.json());
|
app.use(bodyParser.json());
|
||||||
app.use(express.static("public"));
|
app.use(express.static("public"));
|
||||||
@@ -897,6 +1010,77 @@ const buildUserPersonaPrompt = (persona) => {
|
|||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createPersonaChat = (user, persona, userPersonaId = null) => {
|
||||||
|
const chatId = insertChatSessionStmt.run(user, persona.name, persona.id, userPersonaId).lastInsertRowid;
|
||||||
|
return getChatSessionStmt.get(chatId, user);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOrCreatePersonaChat = (user, persona, userPersonaId = null) => {
|
||||||
|
const existing = getChatSessionByParticipantsStmt.get(user, persona.id, userPersonaId, userPersonaId);
|
||||||
|
if (existing) return existing;
|
||||||
|
const sessionCount = countChatSessionsStmt.get(user).count;
|
||||||
|
if (sessionCount >= 10) {
|
||||||
|
throw new Error("CHAT_LIMIT_REACHED");
|
||||||
|
}
|
||||||
|
return createPersonaChat(user, persona, userPersonaId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ensureModelState = (model) => {
|
||||||
|
if (!modelState.has(model)) {
|
||||||
|
modelState.set(model, {activeRequests: 0, timer: null});
|
||||||
|
}
|
||||||
|
return modelState.get(model);
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateModelReply = async (selectedModel, messagesPayload) => {
|
||||||
|
const state = ensureModelState(selectedModel);
|
||||||
|
state.activeRequests++;
|
||||||
|
try {
|
||||||
|
const response = await fetch("https://ai.krishd.ch/api/chat", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {"Content-Type": "application/json"},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: selectedModel,
|
||||||
|
messages: messagesPayload,
|
||||||
|
stream: true
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`AI HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buffer = "";
|
||||||
|
let fullReply = "";
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const {value, done} = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
buffer += decoder.decode(value, {stream: true});
|
||||||
|
const lines = buffer.split("\n");
|
||||||
|
buffer = lines.pop() || "";
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed) continue;
|
||||||
|
const data = JSON.parse(trimmed);
|
||||||
|
if (data.message?.content) fullReply += data.message.content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buffer.trim()) {
|
||||||
|
const data = JSON.parse(buffer.trim());
|
||||||
|
if (data.message?.content) fullReply += data.message.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullReply.trim();
|
||||||
|
} finally {
|
||||||
|
state.activeRequests = Math.max(0, state.activeRequests - 1);
|
||||||
|
scheduleModelUnload(selectedModel);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
app.get("/models", async (req, res) => {
|
app.get("/models", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
if (modelsCache && (Date.now() - modelsCacheAt) < MODELS_TTL_MS) {
|
if (modelsCache && (Date.now() - modelsCacheAt) < MODELS_TTL_MS) {
|
||||||
@@ -924,14 +1108,33 @@ app.get("/chats", requireLogin, (req, res) => {
|
|||||||
app.post("/chats", requireLogin, (req, res) => {
|
app.post("/chats", requireLogin, (req, res) => {
|
||||||
const user = req.session.user;
|
const user = req.session.user;
|
||||||
|
|
||||||
|
const assistantPersonaId = req.body?.assistantPersonaId ? Number(req.body.assistantPersonaId) : null;
|
||||||
|
if (assistantPersonaId) {
|
||||||
|
const persona = getPersonaStmt.get(assistantPersonaId, user);
|
||||||
|
if (!persona || persona.persona_type !== "assistant") {
|
||||||
|
return res.status(404).json({error: "AI Character not found"});
|
||||||
|
}
|
||||||
|
const existingChat = getChatSessionByParticipantsStmt.get(user, assistantPersonaId, null, null);
|
||||||
|
try {
|
||||||
|
const chat = getOrCreatePersonaChat(user, persona, null);
|
||||||
|
return res.json({chat, existing: Boolean(existingChat)});
|
||||||
|
} catch (error) {
|
||||||
|
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."});
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const sessionCount = countChatSessionsStmt.get(user).count;
|
const sessionCount = countChatSessionsStmt.get(user).count;
|
||||||
if (sessionCount >= 10) {
|
if (sessionCount >= 10) {
|
||||||
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."});
|
||||||
}
|
}
|
||||||
|
|
||||||
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).lastInsertRowid;
|
const chatId = insertChatSessionStmt.run(user, title, null, null).lastInsertRowid;
|
||||||
res.json({chat: {id: chatId, title}});
|
const chat = getChatSessionStmt.get(chatId, user);
|
||||||
|
res.json({chat, existing: false});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.put("/chats/:id", requireLogin, (req, res) => {
|
app.put("/chats/:id", requireLogin, (req, res) => {
|
||||||
@@ -966,7 +1169,7 @@ app.get("/chats/:id/messages", requireLogin, (req, res) => {
|
|||||||
return res.status(404).json({error: "Chat not found"});
|
return res.status(404).json({error: "Chat not found"});
|
||||||
}
|
}
|
||||||
const messages = getChatMessagesStmt.all(chatId);
|
const messages = getChatMessagesStmt.all(chatId);
|
||||||
res.json({messages});
|
res.json({messages, chat: session});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/chats/:id/clear", requireLogin, (req, res) => {
|
app.post("/chats/:id/clear", requireLogin, (req, res) => {
|
||||||
@@ -990,7 +1193,6 @@ app.get("/personas", requireLogin, (req, res) => {
|
|||||||
res.json({
|
res.json({
|
||||||
assistantPersonas,
|
assistantPersonas,
|
||||||
userPersonas,
|
userPersonas,
|
||||||
activePersonaId: activePersonas.active_persona_id ?? null,
|
|
||||||
activeUserPersonaId: activePersonas.active_user_persona_id ?? null,
|
activeUserPersonaId: activePersonas.active_user_persona_id ?? null,
|
||||||
publishedPersonaIds
|
publishedPersonaIds
|
||||||
});
|
});
|
||||||
@@ -1054,25 +1256,39 @@ app.delete("/personas/:id", requireLogin, (req, res) => {
|
|||||||
if (result.changes === 0) {
|
if (result.changes === 0) {
|
||||||
return res.status(404).json({error: "Persona not found"});
|
return res.status(404).json({error: "Persona not found"});
|
||||||
}
|
}
|
||||||
if (activePersonas.active_persona_id === personaId) {
|
|
||||||
setActivePersonaStmt.run(user, null);
|
|
||||||
}
|
|
||||||
if (activePersonas.active_user_persona_id === personaId) {
|
if (activePersonas.active_user_persona_id === personaId) {
|
||||||
setActiveUserPersonaStmt.run(user, null);
|
setActiveUserPersonaStmt.run(user, null);
|
||||||
}
|
}
|
||||||
|
clearAssistantPersonaFromChatsStmt.run(user, personaId);
|
||||||
|
clearUserPersonaFromChatsStmt.run(user, personaId);
|
||||||
deleteMarketPersonaByPersonaIdStmt.run(personaId, user);
|
deleteMarketPersonaByPersonaIdStmt.run(personaId, user);
|
||||||
res.json({message: "Persona deleted"});
|
res.json({message: "Persona deleted"});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/personas/:id/equip", requireLogin, (req, res) => {
|
app.post("/personas/:id/chat", requireLogin, (req, res) => {
|
||||||
const user = req.session.user;
|
const user = req.session.user;
|
||||||
const personaId = Number(req.params.id);
|
const personaId = Number(req.params.id);
|
||||||
|
const userPersonaId = req.body?.userPersonaId ? Number(req.body.userPersonaId) : null;
|
||||||
const persona = getPersonaStmt.get(personaId, user);
|
const persona = getPersonaStmt.get(personaId, user);
|
||||||
if (!persona || persona.persona_type !== "assistant") {
|
if (!persona || persona.persona_type !== "assistant") {
|
||||||
return res.status(404).json({error: "Persona not found"});
|
return res.status(404).json({error: "Persona not found"});
|
||||||
}
|
}
|
||||||
setActivePersonaStmt.run(user, personaId);
|
if (userPersonaId) {
|
||||||
res.json({activePersonaId: personaId});
|
const userPersona = getPersonaStmt.get(userPersonaId, user);
|
||||||
|
if (!userPersona || userPersona.persona_type !== "user") {
|
||||||
|
return res.status(404).json({error: "User persona not found"});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let chat;
|
||||||
|
try {
|
||||||
|
chat = getOrCreatePersonaChat(user, persona, userPersonaId);
|
||||||
|
} catch (error) {
|
||||||
|
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."});
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
res.json({chat});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/personas/:id/equip-user", requireLogin, (req, res) => {
|
app.post("/personas/:id/equip-user", requireLogin, (req, res) => {
|
||||||
@@ -1086,12 +1302,6 @@ app.post("/personas/:id/equip-user", requireLogin, (req, res) => {
|
|||||||
res.json({activeUserPersonaId: personaId});
|
res.json({activeUserPersonaId: personaId});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/personas/clear", requireLogin, (req, res) => {
|
|
||||||
const user = req.session.user;
|
|
||||||
setActivePersonaStmt.run(user, null);
|
|
||||||
res.json({activePersonaId: null});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post("/personas/user/clear", requireLogin, (req, res) => {
|
app.post("/personas/user/clear", requireLogin, (req, res) => {
|
||||||
const user = req.session.user;
|
const user = req.session.user;
|
||||||
setActiveUserPersonaStmt.run(user, null);
|
setActiveUserPersonaStmt.run(user, null);
|
||||||
@@ -1192,19 +1402,140 @@ app.post("/personas/market/:id/collect", requireLogin, (req, res) => {
|
|||||||
marketPersona.creator_username
|
marketPersona.creator_username
|
||||||
).lastInsertRowid;
|
).lastInsertRowid;
|
||||||
incrementMarketUsageCountStmt.run(marketId);
|
incrementMarketUsageCountStmt.run(marketId);
|
||||||
let activePersonaId = null;
|
|
||||||
let activeUserPersonaId = null;
|
let activeUserPersonaId = null;
|
||||||
if (req.body?.equip) {
|
if (req.body?.equip) {
|
||||||
if (marketPersona.persona_type === "user") {
|
if (marketPersona.persona_type === "user") {
|
||||||
setActiveUserPersonaStmt.run(user, personaId);
|
setActiveUserPersonaStmt.run(user, personaId);
|
||||||
activeUserPersonaId = personaId;
|
activeUserPersonaId = personaId;
|
||||||
} else {
|
|
||||||
setActivePersonaStmt.run(user, personaId);
|
|
||||||
activePersonaId = personaId;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const persona = getPersonaStmt.get(personaId, user);
|
const persona = getPersonaStmt.get(personaId, user);
|
||||||
res.json({persona, activePersonaId, activeUserPersonaId});
|
res.json({persona, activeUserPersonaId});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/personas/market/:id/chat", requireLogin, (req, res) => {
|
||||||
|
const user = req.session.user;
|
||||||
|
const marketId = Number(req.params.id);
|
||||||
|
const userPersonaId = req.body?.userPersonaId ? Number(req.body.userPersonaId) : null;
|
||||||
|
const marketPersona = getMarketPersonaStmt.get(marketId);
|
||||||
|
if (!marketPersona || marketPersona.persona_type !== "assistant") {
|
||||||
|
return res.status(404).json({error: "Market AI Character not found"});
|
||||||
|
}
|
||||||
|
let userPersona = null;
|
||||||
|
if (userPersonaId) {
|
||||||
|
userPersona = getPersonaStmt.get(userPersonaId, user);
|
||||||
|
if (!userPersona || userPersona.persona_type !== "user") {
|
||||||
|
return res.status(404).json({error: "User persona not found"});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let personaId = null;
|
||||||
|
const existingPersona = getPersonaBySourceMarketStmt.get(user, marketId);
|
||||||
|
if (existingPersona) {
|
||||||
|
personaId = existingPersona.id;
|
||||||
|
} else {
|
||||||
|
personaId = insertMarketPersonaStmt.run(
|
||||||
|
user,
|
||||||
|
marketPersona.name,
|
||||||
|
marketPersona.pronouns,
|
||||||
|
marketPersona.appearance,
|
||||||
|
marketPersona.background,
|
||||||
|
marketPersona.details,
|
||||||
|
marketPersona.example_dialogues,
|
||||||
|
marketPersona.persona_type,
|
||||||
|
marketId,
|
||||||
|
marketPersona.creator_username
|
||||||
|
).lastInsertRowid;
|
||||||
|
incrementMarketUsageCountStmt.run(marketId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const persona = getPersonaStmt.get(personaId, user);
|
||||||
|
let chat;
|
||||||
|
try {
|
||||||
|
chat = getOrCreatePersonaChat(user, persona, userPersonaId);
|
||||||
|
} catch (error) {
|
||||||
|
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."});
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
const existingMessages = getChatMessagesStmt.all(chat.id);
|
||||||
|
if (existingMessages.length > 0) {
|
||||||
|
return res.json({persona, chat, existing: true, generatedInitialMessage: false});
|
||||||
|
}
|
||||||
|
Promise.resolve(generateModelReply("mistral:latest", [
|
||||||
|
{role: "system", content: buildPersonaPrompt(persona)},
|
||||||
|
...(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."}
|
||||||
|
])).then((opener) => {
|
||||||
|
insertChatMessageStmt.run(chat.id, "bot", opener);
|
||||||
|
touchChatSessionStmt.run(chat.id, user);
|
||||||
|
res.json({persona, chat, existing: false, generatedInitialMessage: true, opener});
|
||||||
|
}).catch(() => {
|
||||||
|
res.json({persona, chat, existing: false, generatedInitialMessage: false});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/roleplays/start", requireLogin, async (req, res) => {
|
||||||
|
const user = req.session.user;
|
||||||
|
const selectedModel = req.body?.model || "mistral:latest";
|
||||||
|
const assistantPersonaId = Number(req.body?.assistantPersonaId);
|
||||||
|
const userPersonaId = req.body?.userPersonaId ? Number(req.body.userPersonaId) : null;
|
||||||
|
|
||||||
|
if (!assistantPersonaId) {
|
||||||
|
return res.status(400).json({error: "assistantPersonaId is required"});
|
||||||
|
}
|
||||||
|
|
||||||
|
const assistantPersona = getPersonaStmt.get(assistantPersonaId, user);
|
||||||
|
if (!assistantPersona || assistantPersona.persona_type !== "assistant") {
|
||||||
|
return res.status(404).json({error: "Character not found"});
|
||||||
|
}
|
||||||
|
|
||||||
|
let userPersona = null;
|
||||||
|
if (userPersonaId) {
|
||||||
|
userPersona = getPersonaStmt.get(userPersonaId, user);
|
||||||
|
if (!userPersona || userPersona.persona_type !== "user") {
|
||||||
|
return res.status(404).json({error: "User persona not found"});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let chat;
|
||||||
|
try {
|
||||||
|
chat = getOrCreatePersonaChat(user, assistantPersona, userPersonaId);
|
||||||
|
} catch (error) {
|
||||||
|
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."});
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingMessages = getChatMessagesStmt.all(chat.id);
|
||||||
|
if (existingMessages.length > 0) {
|
||||||
|
return res.json({chat, existing: true, generatedInitialMessage: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
const messagesPayload = [
|
||||||
|
{role: "system", content: buildPersonaPrompt(assistantPersona)}
|
||||||
|
];
|
||||||
|
if (userPersona) {
|
||||||
|
messagesPayload.push({role: "system", content: buildUserPersonaPrompt(userPersona)});
|
||||||
|
}
|
||||||
|
messagesPayload.push({
|
||||||
|
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."
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const opener = await generateModelReply(selectedModel, messagesPayload);
|
||||||
|
if (!opener) {
|
||||||
|
return res.status(500).json({error: "Failed to generate the character opener"});
|
||||||
|
}
|
||||||
|
insertChatMessageStmt.run(chat.id, "bot", opener);
|
||||||
|
touchChatSessionStmt.run(chat.id, user);
|
||||||
|
return res.json({chat, existing: false, generatedInitialMessage: true, opener});
|
||||||
|
} catch {
|
||||||
|
return res.status(500).json({error: "Failed to generate the character opener"});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/chat", requireLogin, async (req, res) => {
|
app.post("/chat", requireLogin, async (req, res) => {
|
||||||
@@ -1222,15 +1553,9 @@ app.post("/chat", requireLogin, async (req, res) => {
|
|||||||
return res.status(404).json({error: "Chat not found"});
|
return res.status(404).json({error: "Chat not found"});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!modelState.has(selectedModel)) {
|
|
||||||
modelState.set(selectedModel, {activeRequests: 0, timer: null});
|
|
||||||
}
|
|
||||||
const state = modelState.get(selectedModel);
|
|
||||||
state.activeRequests++;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const activePersona = getActivePersonaStmt.get(user);
|
const activePersona = getAssistantPersonaForChatStmt.get(targetChatId, user);
|
||||||
const activeUserPersona = getActiveUserPersonaStmt.get(user);
|
const activeUserPersona = getUserPersonaForChatStmt.get(targetChatId, user) || getActiveUserPersonaStmt.get(user);
|
||||||
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});
|
||||||
@@ -1255,41 +1580,7 @@ app.post("/chat", requireLogin, async (req, res) => {
|
|||||||
content: m.content
|
content: m.content
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
const fullReply = await generateModelReply(selectedModel, messagesPayload);
|
||||||
const response = await fetch("https://ai.krishd.ch/api/chat", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {"Content-Type": "application/json"},
|
|
||||||
body: JSON.stringify({
|
|
||||||
model: selectedModel,
|
|
||||||
messages: messagesPayload,
|
|
||||||
stream: true
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`AI HTTP ${response.status}`);
|
|
||||||
}
|
|
||||||
const reader = response.body.getReader();
|
|
||||||
const decoder = new TextDecoder();
|
|
||||||
let buffer = "";
|
|
||||||
let fullReply = "";
|
|
||||||
while (true) {
|
|
||||||
const {value, done} = await reader.read();
|
|
||||||
if (done) break;
|
|
||||||
buffer += decoder.decode(value, {stream: true});
|
|
||||||
let lines = buffer.split("\n");
|
|
||||||
buffer = lines.pop();
|
|
||||||
for (const line of lines) {
|
|
||||||
if (!line.trim()) continue;
|
|
||||||
const json = JSON.parse(line);
|
|
||||||
if (json.done) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (json.message?.content) {
|
|
||||||
fullReply += json.message.content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
insertChatMessageStmt.run(targetChatId, "bot", fullReply);
|
insertChatMessageStmt.run(targetChatId, "bot", fullReply);
|
||||||
touchChatSessionStmt.run(targetChatId, user);
|
touchChatSessionStmt.run(targetChatId, user);
|
||||||
console.log("Reply length:", fullReply.length);
|
console.log("Reply length:", fullReply.length);
|
||||||
@@ -1297,12 +1588,6 @@ app.post("/chat", requireLogin, async (req, res) => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("AI chat error:", err);
|
console.error("AI chat error:", err);
|
||||||
res.status(500).json({error: "AI request failed"});
|
res.status(500).json({error: "AI request failed"});
|
||||||
} finally {
|
|
||||||
state.activeRequests--;
|
|
||||||
|
|
||||||
if (state.activeRequests === 0) {
|
|
||||||
scheduleModelUnload(selectedModel);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user