Revamp overall theme

This commit is contained in:
2026-04-09 12:05:28 +02:00
parent 94d6a8dc2e
commit 602e29e54b
8 changed files with 1435 additions and 198 deletions
+49 -23
View File
@@ -80,9 +80,11 @@
<div class="sidebar-header">
<div>
<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>
<button id="newChat" class="secondary-action" type="button">New</button>
</div>
<label class="field compact-field">
<span>Search</span>
@@ -107,6 +109,11 @@
<div class="chat-title">
<p class="eyebrow">Conversation</p>
<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 class="chat-toolbar">
<button id="renameChat" class="icon-button" type="button">Rename</button>
@@ -144,24 +151,9 @@
</div>
<div class="persona-menu">
<button id="personaMenuButton" class="secondary-action" type="button" aria-expanded="false" aria-controls="personaPopover">
Personas
You
</button>
<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-header">
<div>
@@ -170,11 +162,11 @@
<p class="persona-guidance">Adds context for how the assistant should read you.</p>
</div>
<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>
<a href="/settings" class="secondary-action">Manage</a>
</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>
</div>
@@ -205,18 +197,18 @@
<p class="eyebrow">Persona editor</p>
<h3 id="personaFormTitle">Create persona</h3>
</div>
<button id="personaClose" type="button" class="icon-button">Cancel</button>
<button id="personaClose" type="button" class="icon-button">Close</button>
</div>
<form id="personaForm" class="persona-form">
<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>
<p id="personaFormNotice" class="status hidden"></p>
<div class="persona-form-grid">
<label class="field field-wide">
<span>Persona for</span>
<select id="personaType">
<option value="assistant">AI persona</option>
<option value="assistant">AI Character</option>
<option value="user">Your persona</option>
</select>
</label>
@@ -258,6 +250,40 @@
</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 class="modal-card popup-card" role="dialog" aria-modal="true" aria-labelledby="popupTitle" aria-describedby="popupDescription">
<div class="modal-header">
+110 -11
View File
@@ -15,7 +15,7 @@
<div>
<p class="eyebrow">Shared personas</p>
<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 class="market-header-actions">
<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>
<div class="persona-market-columns market-page-columns">
<div class="persona-market-column">
<h3>AI personas</h3>
<div id="marketPersonaList" class="persona-list"></div>
<section class="settings-layout market-settings-layout">
<aside class="settings-nav" aria-label="Market sections">
<button class="settings-nav-item active" type="button" data-market-view="ai-characters">AI Characters</button>
<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 class="persona-market-column">
<h3>User personas</h3>
<div id="marketUserPersonaList" class="persona-list"></div>
</div>
</div>
</section>
</div>
<div id="marketPreviewModal" class="modal-overlay hidden">
@@ -71,14 +110,74 @@
<h4>Details</h4>
<p id="marketPreviewDetails"></p>
</div>
<div>
<h4>Example dialogues</h4>
<p id="marketPreviewExampleDialogues"></p>
</div>
</div>
</div>
<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="marketPreviewCancel" type="button" class="secondary-action">Cancel</button>
</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>
</body>
</html>
+334 -37
View File
@@ -1,11 +1,37 @@
const $ = (id) => document.getElementById(id);
const marketPersonaList = $("marketPersonaList"), marketUserPersonaList = $("marketUserPersonaList");
const refreshMarketBtn = $("refreshMarket"), marketStatus = $("marketStatus"), marketSearchInput = $("marketSearch");
const marketPreviewModal = $("marketPreviewModal"), marketPreviewName = $("marketPreviewName"), marketPreviewMeta = $("marketPreviewMeta");
const marketPreviewPronouns = $("marketPreviewPronouns"), marketPreviewAppearance = $("marketPreviewAppearance"), marketPreviewBackground = $("marketPreviewBackground"), marketPreviewDetails = $("marketPreviewDetails");
const marketPreviewConfirm = $("marketPreviewConfirm"), marketPreviewCancel = $("marketPreviewCancel"), marketPreviewClose = $("marketPreviewClose");
const marketPersonaList = $("marketPersonaList");
const marketUserPersonaList = $("marketUserPersonaList");
const marketOwnedPersonaList = $("marketOwnedPersonaList");
const refreshMarketBtn = $("refreshMarket");
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]");
let allMarketPersonas = [], pendingMarketPersona = null;
const themes = {
"fakegpt": {name: "FakeGPT"},
@@ -16,9 +42,20 @@ const themes = {
"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") {
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();
} catch {
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 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) {
const nextTheme = themes[themeKey] ? themeKey : "fakegpt";
document.body.dataset.theme = nextTheme;
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 = "") {
@@ -40,9 +81,28 @@ function setMarketStatus(message, state = "") {
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) {
const creator = persona.creator_username || "You";
const pronouns = persona.pronouns ? `Pronouns: ${persona.pronouns}` : "Pronouns: n/a";
return `${persona.creator_username} | ${pronouns}`;
return `${creator} | ${pronouns}`;
}
function formatPersonaField(value) {
@@ -52,6 +112,74 @@ function formatPersonaField(value) {
function closeMarketPreview() {
marketPreviewModal.classList.add("hidden");
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) {
@@ -59,7 +187,7 @@ function renderMarketList(items, listElement, type) {
if (!items.length) {
const empty = document.createElement("p");
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);
return;
}
@@ -77,9 +205,10 @@ function renderMarketList(items, listElement, type) {
titleRow.className = "persona-title-row";
actions.className = "persona-item-actions";
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;
meta.textContent = buildPersonaMeta(persona);
detailsBtn.type = "button";
detailsBtn.textContent = "View";
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() {
const query = marketSearchInput.value.trim().toLowerCase();
if (!query) return allMarketPersonas;
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))
);
}
@@ -104,6 +285,8 @@ function applyMarketFilter() {
const filtered = filterMarketPersonas();
renderMarketList(filtered.filter((persona) => persona.persona_type === "assistant"), marketPersonaList, "assistant");
renderMarketList(filtered.filter((persona) => persona.persona_type === "user"), marketUserPersonaList, "user");
renderOwnedPersonaList();
if (marketSearchInput.value.trim() && !filtered.length) {
setMarketStatus("No personas match your search.", "error");
return;
@@ -111,48 +294,162 @@ function applyMarketFilter() {
setMarketStatus(`${filtered.length} persona${filtered.length === 1 ? "" : "s"} shown.`);
}
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);
marketPreviewConfirm.textContent = persona.persona_type === "assistant" ? "Collect as AI persona" : "Collect as user persona";
marketPreviewModal.classList.remove("hidden");
async function loadOwnedPersonas() {
const res = await get("/personas");
if (res.error) return false;
assistantPersonas = res.assistantPersonas || [];
userPersonas = res.userPersonas || [];
publishedPersonaIds = new Set(res.publishedPersonaIds || []);
return true;
}
async function loadMarketPersonas() {
setMarketStatus("Loading market personas...");
const res = await get("/personas/market");
if (res.error) return setMarketStatus(res.error, "error");
allMarketPersonas = res.personas || [];
setMarketStatus("Loading personas...");
const [marketRes, ownedOk] = await Promise.all([get("/personas/market"), loadOwnedPersonas()]);
if (marketRes.error) {
setMarketStatus(marketRes.error, "error");
return;
}
if (!ownedOk) {
setMarketStatus("Unable to load your personas.", "error");
return;
}
allMarketPersonas = marketRes.personas || [];
applyMarketFilter();
}
async function collectMarketPersona(marketId, personaType) {
setMarketStatus(personaType === "assistant" ? "Collecting AI persona..." : "Collecting user persona...");
async function collectMarketPersona(marketId) {
setMarketStatus("Collecting User Persona...");
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();
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);
marketPreviewCancel.addEventListener("click", closeMarketPreview);
marketViewButtons.forEach((button) => {
button.addEventListener("click", () => setMarketView(button.dataset.marketView));
});
marketCreatePersonaBtn.addEventListener("click", () => openPersonaForm());
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 () => {
if (!pendingMarketPersona) return;
const marketId = pendingMarketPersona.id;
const personaType = pendingMarketPersona.persona_type;
if (pendingMarketPersona.persona_type === "assistant") {
await startMarketPersonaChat(marketId, marketPreviewUserPersonaSelect.value || "");
closeMarketPreview();
return;
}
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");
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
View File
@@ -11,25 +11,30 @@ const loginSubmit = $("loginSubmit"), registerSubmit = $("registerSubmit");
const chatList = $("chatList"), chatSearchInput = $("chatSearch"), newChatBtn = $("newChat");
const renameChatBtn = $("renameChat"), clearChatBtn = $("clearChat"), exportChatBtn = $("exportChat");
const activeChatTitle = $("activeChatTitle"), sessionUser = $("sessionUser");
const chatModePill = $("chatModePill"), chatCharacterPill = $("chatCharacterPill"), chatUserPersonaPill = $("chatUserPersonaPill");
const personaList = $("personaList"), userPersonaList = $("userPersonaList");
const personaForm = $("personaForm"), personaFormTitle = $("personaFormTitle"), personaTypeSelect = $("personaType");
const personaNameInput = $("personaName"), personaPronounsInput = $("personaPronouns"), personaAppearanceInput = $("personaAppearance");
const personaBackgroundInput = $("personaBackground"), personaDetailsInput = $("personaDetails");
const personaExamplesField = $("personaExamplesField"), personaExampleDialoguesInput = $("personaExampleDialogues");
const personaFormNotice = $("personaFormNotice");
const newPersonaBtn = $("newPersona"), newUserPersonaBtn = $("newUserPersona"), clearPersonaBtn = $("clearPersona"), clearUserPersonaBtn = $("clearUserPersona");
const roleplayNewPersonaBtn = $("roleplayNewPersona"), clearUserPersonaBtn = $("clearUserPersona");
const activePersonaStatus = $("activePersonaStatus"), activeUserPersonaStatus = $("activeUserPersonaStatus");
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 popupTitle = $("popupTitle"), popupEyebrow = $("popupEyebrow"), popupDescription = $("popupDescription"), popupField = $("popupField");
const popupInputLabel = $("popupInputLabel"), popupInput = $("popupInput");
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 requestedChatId = Number(new URLSearchParams(window.location.search).get("chat")) || null;
let isProcessing = false, activeChatId = null, editingPersonaId = null, editingPersonaType = "assistant", currentUsername = "";
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 roleplayPresetCharacterId = null;
const themes = {
"fakegpt": {name: "FakeGPT", short: "FG"},
@@ -112,6 +117,7 @@ function addMessage(content, isUser = false, isLoading = false) {
function renderMessages() {
messagesDiv.innerHTML = "";
currentMessages.forEach((msg) => addMessage(msg.content, msg.role === "user"));
updateChatParticipants();
updateChatActionState();
}
@@ -123,6 +129,45 @@ function updateWorkspaceCopy() {
if (sessionUser) {
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() {
@@ -203,8 +248,9 @@ function applyTheme(themeKey, persist = true) {
}
function updateChatActionState() {
const hasChat = Boolean(activeChatId);
renameChatBtn.disabled = !hasChat;
const activeChat = getChatById(activeChatId);
const hasChat = Boolean(activeChat);
renameChatBtn.disabled = !hasChat || Boolean(activeChat?.assistant_persona_id);
clearChatBtn.disabled = !hasChat;
exportChatBtn.disabled = !hasChat || currentMessages.length === 0;
}
@@ -221,11 +267,18 @@ function renderChatList() {
return;
}
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" : ""}`;
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); });
item.append(title, remove);
content.append(title, meta);
item.append(content, remove);
item.addEventListener("click", () => setActiveChat(chat.id));
chatList.appendChild(item);
});
@@ -237,10 +290,13 @@ function updateActivePersonaStatus(items, activeId, statusEl, emptyText, prefix)
}
function setActivePersonaStatus() {
updateActivePersonaStatus(assistantPersonas, activePersonaId, activePersonaStatus, "No AI persona equipped.", "AI persona equipped");
updateActivePersonaStatus(userPersonas, activeUserPersonaId, activeUserPersonaStatus, "No user persona set.", "User persona active");
clearPersonaBtn.disabled = !activePersonaId;
clearUserPersonaBtn.disabled = !activeUserPersonaId;
if (activePersonaStatus) {
activePersonaStatus.textContent = assistantPersonas.length
? "Pick a character, choose a user persona, and start the roleplay."
: "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();
}
@@ -250,14 +306,14 @@ function openPersonaForm(persona = null, personaType = "assistant") {
personaModal.classList.remove("hidden");
if (persona) {
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;
personaNameInput.value = persona.name || ""; personaPronounsInput.value = persona.pronouns || "";
personaAppearanceInput.value = persona.appearance || ""; personaBackgroundInput.value = persona.background || ""; personaDetailsInput.value = persona.details || "";
personaExampleDialoguesInput.value = persona.example_dialogues || "";
} else {
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;
personaNameInput.value = ""; personaPronounsInput.value = ""; personaAppearanceInput.value = ""; personaBackgroundInput.value = ""; personaDetailsInput.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 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() {
modelPopover.classList.add("hidden");
modelMenuButton.setAttribute("aria-expanded", "false");
@@ -330,6 +425,10 @@ function promptPopup(options) {
}
function renderPersonaList(items, activeId, listElement, personaType) {
if (!listElement) {
setActivePersonaStatus();
return;
}
listElement.innerHTML = "";
if (!items.length) {
const empty = document.createElement("p");
@@ -342,10 +441,10 @@ function renderPersonaList(items, activeId, listElement, personaType) {
items.forEach((persona) => {
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");
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";
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],
[publishedPersonaIds.has(persona.id) ? "Update listing" : "Publish", () => publishPersona(persona.id), false],
["Delete", () => deletePersona(persona.id), false]]
@@ -371,26 +470,30 @@ async function loadPersonas() {
const res = await get("/personas");
if (res.error) return setNotice(res.error, "error");
assistantPersonas = res.assistantPersonas || []; userPersonas = res.userPersonas || [];
activePersonaId = res.activePersonaId || null; activeUserPersonaId = res.activeUserPersonaId || null; publishedPersonaIds = new Set(res.publishedPersonaIds || []);
renderPersonaList(assistantPersonas, activePersonaId, personaList, "assistant");
activeUserPersonaId = res.activeUserPersonaId || null; publishedPersonaIds = new Set(res.publishedPersonaIds || []);
renderPersonaList(userPersonas, activeUserPersonaId, userPersonaList, "user");
populateRoleplayStarter();
setActivePersonaStatus();
updateWorkspaceCopy();
}
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 (type === "user") activeUserPersonaId = id; else activePersonaId = id;
renderPersonaList(assistantPersonas, activePersonaId, personaList, "assistant");
activeUserPersonaId = id;
renderPersonaList(userPersonas, activeUserPersonaId, userPersonaList, "user");
setNotice(type === "user" ? "User persona updated." : "AI persona updated.", "success");
setNotice("User persona updated.", "success");
}
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 (type === "user") activeUserPersonaId = null; else activePersonaId = null;
renderPersonaList(assistantPersonas, activePersonaId, personaList, "assistant");
activeUserPersonaId = null;
renderPersonaList(userPersonas, activeUserPersonaId, userPersonaList, "user");
setNotice("Persona cleared.", "success");
}
@@ -409,14 +512,16 @@ async function savePersona() {
const res = editingPersonaId ? await put(`/personas/${editingPersonaId}`, payload) : await post("/personas", payload);
if (res.error) return setPersonaFormNotice(res.error, "error");
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) {
const persona = assistantPersonas.find((item) => item.id === id) || userPersonas.find((item) => item.id === id);
if (!persona) return;
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",
description: `Delete "${persona.name}"? This removes it from your library and any active slot.`,
confirmLabel: "Delete",
@@ -425,7 +530,9 @@ async function deletePersona(id) {
if (!confirmed) return;
const res = await del(`/personas/${id}`);
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 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();
const res = await get(`/chats/${id}/messages`);
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();
}
@@ -453,7 +562,10 @@ async function loadChatSessions() {
const res = await get("/chats");
if (res.error) return setNotice(res.error, "error");
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();
}
@@ -463,6 +575,29 @@ async function createNewChat() {
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) {
const chat = getChatById(id);
const nextTitle = chat ? await promptPopup({
@@ -576,7 +711,7 @@ async function sendMessage() {
if (!message) return;
if (!activeChatId) await createNewChat();
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;
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 () => {
await post("/logout", {}); chatDiv.style.display = "none"; authDiv.style.display = "grid";
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.");
});
sendBtn.addEventListener("click", () => { void sendMessage(); });
@@ -631,8 +766,8 @@ newChatBtn.addEventListener("click", () => { void createNewChat(); });
renameChatBtn.addEventListener("click", () => { if (activeChatId) void renameChat(activeChatId); });
clearChatBtn.addEventListener("click", () => { if (activeChatId) void clearChat(activeChatId); }); exportChatBtn.addEventListener("click", exportCurrentChat);
chatSearchInput.addEventListener("input", renderChatList); modelSelect.addEventListener("change", () => { updateModelHelp(); updateContextRail(); });
newPersonaBtn.addEventListener("click", () => openPersonaForm()); newUserPersonaBtn.addEventListener("click", () => openPersonaForm(null, "user"));
clearPersonaBtn.addEventListener("click", () => { void clearPersona("assistant"); }); clearUserPersonaBtn.addEventListener("click", () => { void clearPersona("user"); });
roleplayNewPersonaBtn.addEventListener("click", () => openPersonaForm(null, "assistant"));
clearUserPersonaBtn.addEventListener("click", () => { void clearPersona("user"); });
personaForm.addEventListener("submit", (event) => { event.preventDefault(); void savePersona(); }); personaCloseBtn.addEventListener("click", closePersonaForm);
personaTypeSelect.addEventListener("change", () => {
const isAssistant = personaTypeSelect.value === "assistant";
@@ -640,6 +775,10 @@ personaTypeSelect.addEventListener("change", () => {
if (!isAssistant) personaExampleDialoguesInput.value = "";
});
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", () => {
if (popupMode === "prompt") {
closePopup(popupInput.value);
@@ -663,6 +802,7 @@ document.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
closeModelPopover();
closePersonaPopover();
closeRoleplayStarter();
closePersonaForm();
closePopup(false);
}
+92 -2
View File
@@ -15,11 +15,11 @@
<div>
<p class="eyebrow">Settings</p>
<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 class="market-header-actions">
<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>
</header>
@@ -28,6 +28,8 @@
<section class="settings-layout">
<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" 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>
</aside>
@@ -82,6 +84,34 @@
</div>
</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">
<div class="settings-panel settings-panel-compact">
<div class="settings-section-header">
@@ -109,6 +139,66 @@
</div>
</section>
</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>
</body>
</html>
+243 -12
View File
@@ -1,7 +1,6 @@
const $ = (id) => document.getElementById(id);
const settingsNotice = $("settingsNotice");
const settingsThemeSelect = $("settingsThemeSelect");
const settingsThemeName = $("settingsThemeName");
const currentUsernameInput = $("currentUsername");
const newUsernameInput = $("newUsername");
const usernamePasswordInput = $("usernamePassword");
@@ -12,6 +11,24 @@ const usernameForm = $("usernameForm");
const passwordForm = $("passwordForm");
const settingsNavItems = document.querySelectorAll("[data-settings-view]");
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 = {
"fakegpt": {name: "FakeGPT"},
@@ -22,6 +39,11 @@ const themes = {
"confusity": {name: "Confusity"}
};
let assistantPersonas = [];
let userPersonas = [];
let publishedPersonaIds = new Set();
let editingPersonaId = null;
async function request(url, data, method = "POST") {
try {
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 = "") {
settingsNotice.textContent = message;
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) {
const nextTheme = themes[themeKey] ? themeKey : "fakegpt";
document.body.dataset.theme = nextTheme;
document.title = "Settings";
settingsThemeSelect.value = nextTheme;
if (settingsThemeName) settingsThemeName.textContent = themes[nextTheme].name;
if (persist) localStorage.setItem("krishd-theme", nextTheme);
}
function setSettingsView(view) {
settingsNavItems.forEach((item) => item.classList.toggle("active", item.dataset.settingsView === 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() {
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) {
setNotice(res.error, "error");
return;
}
currentUsernameInput.value = res.username || "";
newUsernameInput.value = res.username || "";
setNotice("Settings loaded.");
await loadPersonas();
setNotice("Persona deleted.", "success");
}
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) => {
@@ -87,17 +295,17 @@ usernameForm.addEventListener("submit", async (event) => {
passwordForm.addEventListener("submit", async (event) => {
event.preventDefault();
const currentPassword = currentPasswordInput.value;
const newPassword = newPasswordInput.value;
const nextPassword = newPasswordInput.value;
const repeatPassword = repeatPasswordInput.value;
if (!currentPassword || !newPassword || !repeatPassword) {
if (!currentPassword || !nextPassword || !repeatPassword) {
setNotice("Fill in all password fields.", "error");
return;
}
if (newPassword !== repeatPassword) {
if (nextPassword !== repeatPassword) {
setNotice("New passwords do not match.", "error");
return;
}
const res = await request("/settings/password", {currentPassword, newPassword}, "PUT");
const res = await request("/settings/password", {currentPassword, newPassword: nextPassword}, "PUT");
if (res.error) {
setNotice(res.error, "error");
return;
@@ -114,9 +322,32 @@ settingsThemeSelect.addEventListener("change", (event) => {
});
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 () => {
applyTheme(localStorage.getItem("krishd-theme") || "fakegpt", false);
setSettingsView("personal");
await loadProfile();
const params = new URLSearchParams(window.location.search);
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
View File
@@ -384,6 +384,10 @@ h2 {
font-family: "Space Grotesk", sans-serif;
}
.chat-mode-pill {
font-weight: 700;
}
.auth-toggle,
.persona-form,
.input-shell,
@@ -589,6 +593,7 @@ select:focus {
.chat-title-block,
.chat-toolbar,
.chat-header-actions,
.sidebar-header-actions,
.market-header,
.market-header-actions,
.persona-title-row,
@@ -663,6 +668,19 @@ select:focus {
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,
.icon-button {
padding: 8px 12px;
@@ -792,6 +810,18 @@ select:focus {
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 {
flex-wrap: wrap;
justify-content: flex-end;
@@ -1259,7 +1289,7 @@ select:focus {
position: absolute;
top: calc(100% + 10px);
right: 0;
width: min(560px, 88vw);
width: min(460px, 88vw);
border-radius: var(--radius-lg);
padding: 16px;
box-shadow: var(--shadow);
@@ -1273,6 +1303,40 @@ select:focus {
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 {
text-align: left;
}
@@ -1310,6 +1374,10 @@ select:focus {
flex-wrap: wrap;
}
.market-preview-user-field {
width: 100%;
}
.persona-item-actions button {
padding: 8px 11px;
font-size: 0.84rem;
@@ -1515,6 +1583,7 @@ select:focus {
margin: 0;
color: var(--text);
line-height: 1.5;
white-space: pre-wrap;
}
.settings-shell {