Implement a large number of changes, including overhauled styling and a settings page

This commit is contained in:
2026-03-24 11:16:22 +01:00
parent 44efc7818a
commit 169650c71b
8 changed files with 2633 additions and 1792 deletions
+128 -89
View File
@@ -2,122 +2,151 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"/> <meta charset="UTF-8"/>
<title>DeepFake</title> <title>FakeGPT</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&family=Manrope:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css"/> <link rel="stylesheet" href="style.css"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head> </head>
<body> <body>
<div class="app-shell"> <div class="app-shell">
<div id="auth" class="card"> <section id="auth" class="card auth-card auth-card-compact">
<div class="brand"> <div class="auth-hero auth-hero-compact">
<div class="logo">DF</div> <div class="brand">
<div> <div class="logo" data-theme-logo>FG</div>
<p class="eyebrow">Best AI chat in the big 2026</p> <div>
<h1>Welcome to DeepFake</h1> <p class="eyebrow">Private chat</p>
<p class="subtitle">Log in to continue your conversation or create an account to get started.</p> <h1 data-theme-name>FakeGPT</h1>
</div>
</div>
<div class="hero-grid">
<article class="hero-panel">
<p>Log in to continue your chats. Personas, shared presets, and theme changes are available after sign-in.</p>
</article>
</div> </div>
</div> </div>
<div class="auth-toggle"> <div class="auth-panel">
<button class="toggle active" data-target="login">Login</button> <div class="auth-toggle">
<button class="toggle" data-target="register">Register</button> <button class="toggle active" data-target="login">Login</button>
</div> <button class="toggle" data-target="register">Register</button>
</div>
<div id="loginScreen" class="auth-screen active"> <div id="loginScreen" class="auth-screen active">
<form id="loginForm" class="auth-form"> <form id="loginForm" class="auth-form">
<label class="field"> <label class="field">
<span>Username</span> <span>Username</span>
<input id="loginUsername" placeholder="Enter your username" autocomplete="username"/> <input id="loginUsername" placeholder="Enter your username" autocomplete="username"/>
</label> </label>
<label class="field"> <label class="field">
<span>Password</span> <span>Password</span>
<input id="loginPassword" type="password" placeholder="Enter your password" <input id="loginPassword" type="password" placeholder="Enter your password" autocomplete="current-password"/>
autocomplete="current-password"/> </label>
</label> <button id="loginSubmit" type="submit" class="primary-action">Sign in</button>
<button id="loginSubmit" type="submit" class="primary-action">Sign in</button> </form>
</form> </div>
</div>
<div id="registerScreen" class="auth-screen"> <div id="registerScreen" class="auth-screen">
<form id="registerForm" class="auth-form"> <form id="registerForm" class="auth-form">
<label class="field"> <label class="field">
<span>Create username</span> <span>Create username</span>
<input id="registerUsername" placeholder="Choose a username" autocomplete="new-username"/> <input id="registerUsername" placeholder="Choose a username" autocomplete="new-username"/>
</label> </label>
<label class="field"> <label class="field">
<span>Create password</span> <span>Create password</span>
<input id="registerPassword" type="password" placeholder="Create a password" <input id="registerPassword" type="password" placeholder="Create a password" autocomplete="new-password"/>
autocomplete="new-password"/> </label>
</label> <div class="consent-container">
<div class="consent-container"> <input type="checkbox" id="consentCheckbox" required/>
<input type="checkbox" id="consentCheckbox" required/> <label for="consentCheckbox">
<label for="consentCheckbox"> I agree to the
I agree to the <a href="/tos.html" target="_blank">Terms of Service</a> and
<a href="/tos.html" target="_blank">Terms of Service</a> and <a href="/privacy.html" target="_blank">Privacy Policy</a>.
<a href="/privacy.html" target="_blank">Privacy Policy</a>. </label>
</div>
<button id="registerSubmit" type="submit" class="primary-action">Create account</button>
</form>
</div>
<p id="authMsg" class="status"></p>
</div>
</section>
<section id="chat" class="card chat-card chat-focus-card" style="display:none;">
<div class="chat-layout chat-layout-focus">
<aside class="chat-sidebar">
<div class="sidebar-top">
<div class="sidebar-header">
<div>
<p class="eyebrow">Chats</p>
<h2>Saved threads</h2>
</div>
<button id="newChat" class="secondary-action" type="button">New</button>
</div>
<label class="field compact-field">
<span>Search</span>
<input id="chatSearch" type="search" placeholder="Filter chat titles"/>
</label> </label>
</div> </div>
<button id="registerSubmit" type="submit" class="primary-action">Create account</button>
</form>
</div>
<p id="authMsg" class="status"></p>
</div>
<div id="chat" class="card" style="display:none;">
<div class="chat-layout">
<aside class="chat-sidebar">
<div class="sidebar-header">
<div>
<p class="eyebrow">Conversations</p>
<h2>Chats</h2>
</div>
<button id="newChat" class="secondary-action">New</button>
</div>
<div id="chatList" class="chat-list"></div> <div id="chatList" class="chat-list"></div>
<div class="sidebar-footer sidebar-footer-stack">
<a href="/market" class="secondary-action">Persona Market</a>
<a href="/settings" class="secondary-action">Settings</a>
<div class="session-chip">
<span class="session-chip-label">Signed in as</span>
<strong id="sessionUser">-</strong>
</div>
<button id="logout" type="button">Logout</button>
</div>
</aside> </aside>
<section class="chat-main">
<div class="chat-header"> <section class="chat-main chat-main-focus">
<div class="chat-title"> <header class="chat-header chat-header-focus">
<h2 id="activeChatTitle">New chat</h2> <div class="chat-title-block">
<button id="renameChat" class="icon-button" type="button">Rename</button> <div class="chat-title">
<p class="eyebrow">Conversation</p>
<h2 id="activeChatTitle">New chat</h2>
</div>
<div class="chat-toolbar">
<button id="renameChat" class="icon-button" type="button">Rename</button>
<button id="clearChat" class="icon-button" type="button">Clear</button>
<button id="exportChat" class="icon-button" type="button">Export</button>
</div>
</div> </div>
<div class="chat-header-actions"> <div class="chat-header-actions">
<div class="model-select"> <div class="model-select">
<label for="model">Model:</label> <label for="model">Model</label>
<select id="model"> <select id="model">
<option disabled selected>Loading models...</option> <option disabled selected>Loading models...</option>
</select> </select>
</div> </div>
<a href="/market.html" class="secondary-action market-link">Persona Market</a>
<div class="persona-menu"> <div class="persona-menu">
<button id="personaMenuButton" class="secondary-action" type="button" <button id="personaMenuButton" class="secondary-action" type="button" aria-expanded="false" aria-controls="personaPopover">
aria-expanded="false" aria-controls="personaPopover">
Personas Personas
</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">
<div class="persona-section-header"> <div class="persona-section-header">
<div> <div>
<p class="eyebrow">Roleplay</p> <p class="eyebrow">Assistant</p>
<h2>AI Persona</h2> <h2>AI persona</h2>
<p class="persona-guidance">Controls how the assistant sounds and behaves.</p> <p class="persona-guidance">Changes the assistant's tone and character.</p>
</div> </div>
<div class="persona-section-actions"> <div class="persona-section-actions">
<button id="newPersona" class="secondary-action" type="button">New AI</button> <button id="newPersona" class="secondary-action" type="button">New AI</button>
<button id="clearPersona" class="secondary-action" type="button">Clear</button> <button id="clearPersona" class="secondary-action" type="button">Clear</button>
</div> </div>
</div> </div>
<p id="activePersonaStatus" class="status persona-status">No persona equipped.</p> <p id="activePersonaStatus" class="status persona-status">No AI persona equipped.</p>
<div id="personaList" class="persona-list"></div> <div id="personaList" class="persona-list"></div>
</div> </div>
<div class="persona-section"> <div class="persona-section">
<div class="persona-section-header"> <div class="persona-section-header">
<div> <div>
<p class="eyebrow">Roleplay</p> <p class="eyebrow">User</p>
<h2>Your Persona</h2> <h2>Your persona</h2>
<p class="persona-guidance">Defines how the assistant should portray 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="newUserPersona" class="secondary-action" type="button">New you</button>
@@ -130,20 +159,30 @@
</div> </div>
</div> </div>
</div> </div>
</header>
<div id="messages" class="messages-panel messages-panel-focus"></div>
<div id="emptyState" class="empty-state">
<p class="eyebrow">No messages yet</p>
<h3>Write something when you're ready.</h3>
<p>This can stay a plain chat. Personas and shared presets are optional.</p>
</div> </div>
<div id="messages"></div>
<div class="input-container"> <div class="input-shell input-shell-focus">
<textarea id="msgInput" placeholder="Type your message..."></textarea> <div class="input-meta">
<button id="send"></button> <p class="composer-hint">Enter sends. Shift+Enter adds a new line.</p>
</div> </div>
<div class="logout-container"> <div class="input-container">
<button id="deleteChat" class="danger">Delete Chat</button> <textarea id="msgInput" placeholder="Type your message..."></textarea>
<button id="logout">Logout</button> <button id="send" type="button" aria-label="Send message"></button>
</div>
</div> </div>
</section> </section>
</div> </div>
</div> </section>
</div> </div>
<div id="personaModal" class="modal-overlay hidden"> <div id="personaModal" class="modal-overlay hidden">
<div class="modal-card"> <div class="modal-card">
<div class="modal-header"> <div class="modal-header">
@@ -158,7 +197,7 @@
<option value="user">Your persona</option> <option value="user">Your persona</option>
</select> </select>
</label> </label>
<p class="persona-helper">AI personas shape how the assistant speaks. Your persona describes how you should be portrayed.</p> <p class="persona-helper">AI personas shape the assistant. Your persona gives the model user-side context.</p>
<label class="field"> <label class="field">
<span>Name</span> <span>Name</span>
<input id="personaName" placeholder="Persona name" required/> <input id="personaName" placeholder="Persona name" required/>
@@ -177,8 +216,7 @@
</label> </label>
<label class="field"> <label class="field">
<span>Details</span> <span>Details</span>
<textarea id="personaDetails" rows="3" <textarea id="personaDetails" rows="3" placeholder="Personality traits, voice, quirks, goals"></textarea>
placeholder="Personality traits, voice, or quirks"></textarea>
</label> </label>
<div class="persona-actions"> <div class="persona-actions">
<button id="personaSave" type="submit" class="primary-action">Save persona</button> <button id="personaSave" type="submit" class="primary-action">Save persona</button>
@@ -186,6 +224,7 @@
</form> </form>
</div> </div>
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js" defer></script> <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.6/dist/purify.min.js" defer></script> <script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.6/dist/purify.min.js" defer></script>
<script src="script.js" defer></script> <script src="script.js" defer></script>
+19 -13
View File
@@ -2,41 +2,47 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"/> <meta charset="UTF-8"/>
<title>Persona Market</title> <title>FakeGPT Market</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&family=Manrope:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css"/> <link rel="stylesheet" href="style.css"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head> </head>
<body> <body>
<div class="market-shell card"> <div class="market-shell card market-shell-simple">
<header class="market-header"> <header class="market-header market-header-simple">
<div> <div>
<p class="eyebrow">Community</p> <p class="eyebrow">Shared personas</p>
<h1>Persona Market</h1> <h1><span data-theme-name>FakeGPT</span> Market</h1>
<p class="subtitle">Browse shared personas and choose who should embody them.</p> <p class="subtitle">A plain list of personas other people decided to share.</p>
<div class="market-search">
<label for="marketSearch">Search personas</label>
<input id="marketSearch" type="search" placeholder="Search by name, creator, or details"/>
</div>
</div> </div>
<div class="market-header-actions"> <div class="market-header-actions">
<button id="refreshMarket" class="secondary-action" type="button">Refresh listings</button> <button id="refreshMarket" class="secondary-action" type="button">Refresh</button>
<a href="/settings" class="secondary-action">Settings</a>
<a href="/" class="secondary-action">Back to chat</a> <a href="/" class="secondary-action">Back to chat</a>
</div> </div>
</header> </header>
<div class="market-search">
<label for="marketSearch">Search personas</label>
<input id="marketSearch" type="search" placeholder="Search by name, creator, or description"/>
</div>
<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"> <div class="persona-market-columns market-page-columns">
<div class="persona-market-column"> <div class="persona-market-column">
<h3>AI personas</h3> <h3>AI personas</h3>
<p class="persona-guidance">These shape how the assistant speaks and behaves.</p>
<div id="marketPersonaList" class="persona-list"></div> <div id="marketPersonaList" class="persona-list"></div>
</div> </div>
<div class="persona-market-column"> <div class="persona-market-column">
<h3>User personas</h3> <h3>User personas</h3>
<p class="persona-guidance">These describe how the assistant should portray you.</p>
<div id="marketUserPersonaList" class="persona-list"></div> <div id="marketUserPersonaList" class="persona-list"></div>
</div> </div>
</div> </div>
</div> </div>
<div id="marketPreviewModal" class="modal-overlay hidden"> <div id="marketPreviewModal" class="modal-overlay hidden">
<div class="modal-card market-preview-card"> <div class="modal-card market-preview-card">
<div class="modal-header"> <div class="modal-header">
+108 -176
View File
@@ -1,114 +1,117 @@
const marketPersonaList = document.getElementById("marketPersonaList"); const $ = (id) => document.getElementById(id);
const marketUserPersonaList = document.getElementById("marketUserPersonaList"); const marketPersonaList = $("marketPersonaList"), marketUserPersonaList = $("marketUserPersonaList");
const refreshMarketBtn = document.getElementById("refreshMarket"); const refreshMarketBtn = $("refreshMarket"), marketStatus = $("marketStatus"), marketSearchInput = $("marketSearch");
const marketStatus = document.getElementById("marketStatus"); const marketPreviewModal = $("marketPreviewModal"), marketPreviewName = $("marketPreviewName"), marketPreviewMeta = $("marketPreviewMeta");
const marketSearchInput = document.getElementById("marketSearch"); const marketPreviewPronouns = $("marketPreviewPronouns"), marketPreviewAppearance = $("marketPreviewAppearance"), marketPreviewBackground = $("marketPreviewBackground"), marketPreviewDetails = $("marketPreviewDetails");
const marketPreviewModal = document.getElementById("marketPreviewModal"); const marketPreviewConfirm = $("marketPreviewConfirm"), marketPreviewCancel = $("marketPreviewCancel"), marketPreviewClose = $("marketPreviewClose");
const marketPreviewName = document.getElementById("marketPreviewName"); const themeNameTargets = document.querySelectorAll("[data-theme-name]");
const marketPreviewMeta = document.getElementById("marketPreviewMeta"); let allMarketPersonas = [], pendingMarketPersona = null;
const marketPreviewPronouns = document.getElementById("marketPreviewPronouns");
const marketPreviewAppearance = document.getElementById("marketPreviewAppearance"); const themes = {
const marketPreviewBackground = document.getElementById("marketPreviewBackground"); "fakegpt": {name: "FakeGPT"},
const marketPreviewDetails = document.getElementById("marketPreviewDetails"); "fraud": {name: "Fraud"},
const marketPreviewConfirm = document.getElementById("marketPreviewConfirm"); "germini": {name: "Germini"},
const marketPreviewCancel = document.getElementById("marketPreviewCancel"); "slopilot": {name: "Slopilot"},
const marketPreviewClose = document.getElementById("marketPreviewClose"); "beta-ai": {name: "Beta AI"},
let allMarketPersonas = []; "confusity": {name: "Confusity"}
let pendingMarketPersona = 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, {method, headers: {"Content-Type": "application/json"}, body: data ? JSON.stringify(data) : undefined});
method,
headers: {"Content-Type": "application/json"},
body: data ? JSON.stringify(data) : undefined
});
return await res.json(); return await res.json();
} catch (err) { } catch {
return {error: "Network error — please try again."}; return {error: "Network error - please try again."};
} }
} }
async function post(url, data) { const get = (url) => request(url, null, "GET");
return request(url, data, "POST"); const post = (url, data) => request(url, data, "POST");
}
async function get(url) { function applyTheme(themeKey) {
try { const nextTheme = themes[themeKey] ? themeKey : "fakegpt";
const res = await fetch(url); document.body.dataset.theme = nextTheme;
return await res.json(); document.title = `${themes[nextTheme].name} Market`;
} catch (err) { themeNameTargets.forEach((target) => { target.textContent = themes[nextTheme].name; });
return {error: "Network error — please try again."};
}
} }
function setMarketStatus(message, state = "") { function setMarketStatus(message, state = "") {
if (!marketStatus) return;
marketStatus.textContent = message; marketStatus.textContent = message;
marketStatus.className = state ? `status persona-status ${state}` : "status persona-status"; marketStatus.className = state ? `status persona-status ${state}` : "status persona-status";
} }
function buildPersonaMeta(persona) { function buildPersonaMeta(persona) {
const pronouns = persona.pronouns ? `Pronouns: ${persona.pronouns}` : "Pronouns: —"; const pronouns = persona.pronouns ? `Pronouns: ${persona.pronouns}` : "Pronouns: n/a";
const usageCount = Number(persona.usage_count || 0); return `${persona.creator_username} | ${pronouns}`;
const usageLabel = `Used ${usageCount} ${usageCount === 1 ? "time" : "times"}`;
return `By ${persona.creator_username} • ${pronouns} • ${usageLabel}`;
}
function renderMarketList(personaItems, listElement, personaType) {
if (!listElement) return;
listElement.innerHTML = "";
if (!personaItems.length) {
const empty = document.createElement("p");
empty.className = "status persona-status";
empty.textContent = personaType === "assistant"
? "No AI personas listed yet. Publish the first one!"
: "No user personas listed yet. Publish the first one!";
listElement.appendChild(empty);
return;
}
personaItems.forEach(persona => {
const item = document.createElement("div");
item.className = "persona-item";
const titleRow = document.createElement("div");
titleRow.className = "persona-title-row";
const title = document.createElement("h4");
title.textContent = persona.name;
const roleTag = document.createElement("span");
roleTag.className = `persona-role-tag ${personaType === "assistant" ? "ai" : "user"}`;
roleTag.textContent = personaType === "assistant" ? "AI" : "You";
titleRow.appendChild(title);
titleRow.appendChild(roleTag);
const meta = document.createElement("p");
meta.textContent = buildPersonaMeta(persona);
const actions = document.createElement("div");
actions.className = "persona-item-actions";
const getBtn = document.createElement("button");
getBtn.type = "button";
getBtn.textContent = "View details";
getBtn.addEventListener("click", () => openMarketPreview(persona));
actions.appendChild(getBtn);
item.appendChild(titleRow);
item.appendChild(meta);
item.appendChild(actions);
listElement.appendChild(item);
});
} }
function formatPersonaField(value) { function formatPersonaField(value) {
return value && value.trim() ? value.trim() : "Not provided."; return value && value.trim() ? value.trim() : "Not provided.";
} }
function closeMarketPreview() {
marketPreviewModal.classList.add("hidden");
pendingMarketPersona = null;
}
function renderMarketList(items, listElement, type) {
listElement.innerHTML = "";
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.";
listElement.appendChild(empty);
return;
}
items.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");
const detailsBtn = document.createElement("button");
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" : "You";
title.textContent = persona.name;
meta.textContent = buildPersonaMeta(persona);
detailsBtn.type = "button";
detailsBtn.textContent = "View";
detailsBtn.addEventListener("click", () => openMarketPreview(persona));
titleRow.append(title, tag);
actions.appendChild(detailsBtn);
item.append(titleRow, meta, actions);
listElement.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]
.some((field) => field && field.toLowerCase().includes(query))
);
}
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");
if (marketSearchInput.value.trim() && !filtered.length) {
setMarketStatus("No personas match your search.", "error");
return;
}
setMarketStatus(`${filtered.length} persona${filtered.length === 1 ? "" : "s"} shown.`);
}
function openMarketPreview(persona) { function openMarketPreview(persona) {
if (!marketPreviewModal) return;
pendingMarketPersona = persona; pendingMarketPersona = persona;
marketPreviewName.textContent = persona.name; marketPreviewName.textContent = persona.name;
marketPreviewMeta.textContent = buildPersonaMeta(persona); marketPreviewMeta.textContent = buildPersonaMeta(persona);
@@ -116,110 +119,39 @@ function openMarketPreview(persona) {
marketPreviewAppearance.textContent = formatPersonaField(persona.appearance); marketPreviewAppearance.textContent = formatPersonaField(persona.appearance);
marketPreviewBackground.textContent = formatPersonaField(persona.background); marketPreviewBackground.textContent = formatPersonaField(persona.background);
marketPreviewDetails.textContent = formatPersonaField(persona.details); marketPreviewDetails.textContent = formatPersonaField(persona.details);
marketPreviewConfirm.textContent = marketPreviewConfirm.textContent = persona.persona_type === "assistant" ? "Collect as AI persona" : "Collect as user persona";
persona.persona_type === "assistant" ? "Get & equip as AI" : "Get & equip as You";
marketPreviewModal.classList.remove("hidden"); marketPreviewModal.classList.remove("hidden");
} }
function closeMarketPreview() {
if (!marketPreviewModal) return;
marketPreviewModal.classList.add("hidden");
pendingMarketPersona = null;
}
function filterMarketPersonas() {
const query = marketSearchInput?.value.trim().toLowerCase() || "";
if (!query) return allMarketPersonas;
return allMarketPersonas.filter(persona => {
const fields = [
persona.name,
persona.creator_username,
persona.pronouns,
persona.appearance,
persona.background,
persona.details
];
return fields.some(field => field && field.toLowerCase().includes(query));
});
}
function applyMarketFilter() {
const filtered = filterMarketPersonas();
const assistantMarket = filtered.filter(persona => persona.persona_type === "assistant");
const userMarket = filtered.filter(persona => persona.persona_type === "user");
renderMarketList(assistantMarket, marketPersonaList, "assistant");
renderMarketList(userMarket, marketUserPersonaList, "user");
if (marketSearchInput?.value.trim() && !filtered.length) {
setMarketStatus("No personas match your search.", "error");
return;
}
setMarketStatus("Choose an AI persona or a user persona to equip.");
}
async function loadMarketPersonas() { async function loadMarketPersonas() {
setMarketStatus("Loading market personas..."); setMarketStatus("Loading market personas...");
const res = await get("/personas/market"); const res = await get("/personas/market");
if (res.error) { if (res.error) return setMarketStatus(res.error, "error");
setMarketStatus(res.error, "error");
return;
}
allMarketPersonas = res.personas || []; allMarketPersonas = res.personas || [];
applyMarketFilter(); applyMarketFilter();
} }
async function collectMarketPersona(marketId, personaType) { async function collectMarketPersona(marketId, personaType) {
setMarketStatus( setMarketStatus(personaType === "assistant" ? "Collecting AI persona..." : "Collecting user persona...");
personaType === "assistant"
? "Equipping the AI persona..."
: "Equipping the 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) { if (res.error) return setMarketStatus(res.error, "error");
setMarketStatus(res.error, "error");
return;
}
await loadMarketPersonas(); await loadMarketPersonas();
setMarketStatus( setMarketStatus(personaType === "assistant" ? "AI persona collected." : "User persona collected.", "success");
personaType === "assistant"
? "AI persona equipped. Head back to chat to use it."
: "User persona equipped. Head back to chat to use it.",
"success"
);
} }
if (refreshMarketBtn) { refreshMarketBtn.addEventListener("click", loadMarketPersonas);
refreshMarketBtn.addEventListener("click", () => loadMarketPersonas()); marketSearchInput.addEventListener("input", applyMarketFilter);
} marketPreviewCancel.addEventListener("click", closeMarketPreview);
marketPreviewClose.addEventListener("click", closeMarketPreview);
if (marketSearchInput) { marketPreviewModal.addEventListener("click", (event) => { if (event.target === marketPreviewModal) closeMarketPreview(); });
marketSearchInput.addEventListener("input", () => applyMarketFilter()); marketPreviewConfirm.addEventListener("click", async () => {
} if (!pendingMarketPersona) return;
const {id, persona_type} = pendingMarketPersona;
if (marketPreviewCancel) { closeMarketPreview();
marketPreviewCancel.addEventListener("click", closeMarketPreview); await collectMarketPersona(id, persona_type);
} });
if (marketPreviewClose) {
marketPreviewClose.addEventListener("click", closeMarketPreview);
}
if (marketPreviewModal) {
marketPreviewModal.addEventListener("click", (event) => {
if (event.target === marketPreviewModal) {
closeMarketPreview();
}
});
}
if (marketPreviewConfirm) {
marketPreviewConfirm.addEventListener("click", async () => {
if (!pendingMarketPersona) return;
const {id, persona_type} = pendingMarketPersona;
closeMarketPreview();
await collectMarketPersona(id, persona_type);
});
}
window.addEventListener("load", () => { window.addEventListener("load", () => {
applyTheme(localStorage.getItem("krishd-theme") || "fakegpt");
loadMarketPersonas(); loadMarketPersonas();
}); });
+309 -606
View File
File diff suppressed because it is too large Load Diff
+114
View File
@@ -0,0 +1,114 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Settings</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&family=Manrope:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div class="settings-shell card">
<header class="market-header market-header-simple">
<div>
<p class="eyebrow">Settings</p>
<h1>Preferences</h1>
<p class="subtitle">Manage account details 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>
</div>
</header>
<p id="settingsNotice" class="status">Loading settings...</p>
<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="design">Design</button>
</aside>
<div class="settings-content">
<section class="settings-view active" data-settings-panel="personal">
<div class="settings-panel">
<div class="settings-section-header">
<div>
<p class="eyebrow">Profile</p>
<h2>Username</h2>
</div>
</div>
<form id="usernameForm" class="settings-form-stack">
<label class="field">
<span>Current username</span>
<input id="currentUsername" readonly/>
</label>
<label class="field">
<span>New username</span>
<input id="newUsername" autocomplete="username"/>
</label>
<label class="field">
<span>Current password</span>
<input id="usernamePassword" type="password" autocomplete="current-password"/>
</label>
<button id="saveUsername" type="submit">Save username</button>
</form>
</div>
<div class="settings-panel">
<div class="settings-section-header">
<div>
<p class="eyebrow">Security</p>
<h2>Password</h2>
</div>
</div>
<form id="passwordForm" class="settings-form-stack">
<label class="field">
<span>Current password</span>
<input id="currentPassword" type="password" autocomplete="current-password"/>
</label>
<label class="field">
<span>New password</span>
<input id="newPassword" type="password" autocomplete="new-password"/>
</label>
<label class="field">
<span>Repeat new password</span>
<input id="repeatPassword" type="password" autocomplete="new-password"/>
</label>
<button id="savePassword" type="submit">Save password</button>
</form>
</div>
</section>
<section class="settings-view" data-settings-panel="design">
<div class="settings-panel settings-panel-compact">
<div class="settings-section-header">
<div>
<p class="eyebrow">Appearance</p>
<h2>Theme</h2>
<p class="subtitle">Themes are only changed here.</p>
</div>
</div>
<div class="settings-theme-row">
<label class="field">
<span>Theme</span>
<select id="settingsThemeSelect">
<option value="fakegpt">FakeGPT</option>
<option value="fraud">Fraud</option>
<option value="germini">Germini</option>
<option value="slopilot">Slopilot</option>
<option value="beta-ai">Beta AI</option>
<option value="confusity">Confusity</option>
</select>
</label>
</div>
</div>
</section>
</div>
</section>
</div>
<script src="settings.js" defer></script>
</body>
</html>
+122
View File
@@ -0,0 +1,122 @@
const $ = (id) => document.getElementById(id);
const settingsNotice = $("settingsNotice");
const settingsThemeSelect = $("settingsThemeSelect");
const settingsThemeName = $("settingsThemeName");
const currentUsernameInput = $("currentUsername");
const newUsernameInput = $("newUsername");
const usernamePasswordInput = $("usernamePassword");
const currentPasswordInput = $("currentPassword");
const newPasswordInput = $("newPassword");
const repeatPasswordInput = $("repeatPassword");
const usernameForm = $("usernameForm");
const passwordForm = $("passwordForm");
const settingsNavItems = document.querySelectorAll("[data-settings-view]");
const settingsPanels = document.querySelectorAll("[data-settings-panel]");
const themes = {
"fakegpt": {name: "FakeGPT"},
"fraud": {name: "Fraud"},
"germini": {name: "Germini"},
"slopilot": {name: "Slopilot"},
"beta-ai": {name: "Beta AI"},
"confusity": {name: "Confusity"}
};
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
});
return await res.json();
} catch {
return {error: "Network error - please try again."};
}
}
function setNotice(message, state = "") {
settingsNotice.textContent = message;
settingsNotice.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));
}
async function loadProfile() {
const res = await request("/settings/profile", null, "GET");
if (res.error) {
setNotice(res.error, "error");
return;
}
currentUsernameInput.value = res.username || "";
newUsernameInput.value = res.username || "";
setNotice("Settings loaded.");
}
usernameForm.addEventListener("submit", async (event) => {
event.preventDefault();
const username = newUsernameInput.value.trim();
const password = usernamePasswordInput.value;
if (!username || !password) {
setNotice("New username and current password are required.", "error");
return;
}
const res = await request("/settings/username", {username, password}, "PUT");
if (res.error) {
setNotice(res.error, "error");
return;
}
currentUsernameInput.value = res.username;
newUsernameInput.value = res.username;
usernamePasswordInput.value = "";
setNotice("Username updated.", "success");
});
passwordForm.addEventListener("submit", async (event) => {
event.preventDefault();
const currentPassword = currentPasswordInput.value;
const newPassword = newPasswordInput.value;
const repeatPassword = repeatPasswordInput.value;
if (!currentPassword || !newPassword || !repeatPassword) {
setNotice("Fill in all password fields.", "error");
return;
}
if (newPassword !== repeatPassword) {
setNotice("New passwords do not match.", "error");
return;
}
const res = await request("/settings/password", {currentPassword, newPassword}, "PUT");
if (res.error) {
setNotice(res.error, "error");
return;
}
currentPasswordInput.value = "";
newPasswordInput.value = "";
repeatPasswordInput.value = "";
setNotice("Password updated.", "success");
});
settingsThemeSelect.addEventListener("change", (event) => {
applyTheme(event.target.value);
setNotice("Theme updated.", "success");
});
settingsNavItems.forEach((item) => item.addEventListener("click", () => setSettingsView(item.dataset.settingsView)));
window.addEventListener("load", async () => {
applyTheme(localStorage.getItem("krishd-theme") || "fakegpt", false);
setSettingsView("personal");
await loadProfile();
});
+1668 -903
View File
File diff suppressed because it is too large Load Diff
+165 -5
View File
@@ -298,6 +298,8 @@ db.prepare(`
const insertUserStmt = db.prepare("INSERT INTO users (username, password) VALUES (?, ?)"); const insertUserStmt = db.prepare("INSERT INTO users (username, password) VALUES (?, ?)");
const getUserStmt = db.prepare("SELECT username, password FROM users WHERE username = ?"); const getUserStmt = db.prepare("SELECT username, password FROM users WHERE username = ?");
const updateUsernameStmt = db.prepare("UPDATE users SET username = ? WHERE username = ?");
const updatePasswordStmt = db.prepare("UPDATE users SET password = ? WHERE username = ?");
const getLegacyChatsStmt = db.prepare(` const getLegacyChatsStmt = db.prepare(`
SELECT role, content SELECT role, content
FROM chats FROM chats
@@ -529,6 +531,71 @@ const listPublishedPersonaIdsStmt = db.prepare(`
WHERE creator_username = ? WHERE creator_username = ?
`); `);
const getChatMessageCountForUserStmt = db.prepare(`
SELECT COUNT(*) as count
FROM chat_messages cm
JOIN chat_sessions cs ON cs.id = cm.chat_id
WHERE cs.username = ?
`);
const getPersonaCountForUserStmt = db.prepare(`
SELECT COUNT(*) as count
FROM personas
WHERE username = ?
`);
const getPublishedPersonaCountForUserStmt = db.prepare(`
SELECT COUNT(*) as count
FROM persona_market
WHERE creator_username = ?
`);
const updateLegacyChatsUsernameStmt = db.prepare(`
UPDATE chats
SET username = ?
WHERE username = ?
`);
const updateChatSessionsUsernameStmt = db.prepare(`
UPDATE chat_sessions
SET username = ?
WHERE username = ?
`);
const updatePersonasUsernameStmt = db.prepare(`
UPDATE personas
SET username = ?
WHERE username = ?
`);
const updatePersonasSourceCreatorStmt = db.prepare(`
UPDATE personas
SET source_creator_username = ?
WHERE source_creator_username = ?
`);
const updateUserSettingsUsernameStmt = db.prepare(`
UPDATE user_settings
SET username = ?
WHERE username = ?
`);
const updatePersonaMarketCreatorStmt = db.prepare(`
UPDATE persona_market
SET creator_username = ?
WHERE creator_username = ?
`);
const renameUserTransaction = db.transaction((currentUsername, nextUsername) => {
updateUsernameStmt.run(nextUsername, currentUsername);
updateLegacyChatsUsernameStmt.run(nextUsername, currentUsername);
updateChatSessionsUsernameStmt.run(nextUsername, currentUsername);
updatePersonasUsernameStmt.run(nextUsername, currentUsername);
updatePersonasSourceCreatorStmt.run(nextUsername, currentUsername);
updateUserSettingsUsernameStmt.run(nextUsername, currentUsername);
updatePersonaMarketCreatorStmt.run(nextUsername, currentUsername);
});
const deleteMarketPersonaByPersonaIdStmt = db.prepare(` const deleteMarketPersonaByPersonaIdStmt = db.prepare(`
DELETE FROM persona_market DELETE FROM persona_market
WHERE persona_id = ? WHERE persona_id = ?
@@ -640,6 +707,76 @@ app.post("/logout", (req, res) => {
req.session.destroy(() => res.json({message: "Logged out"})); req.session.destroy(() => res.json({message: "Logged out"}));
}); });
const requireLogin = (req, res, next) => {
if (!req.session.user) return res.status(403).json({error: "Not logged in"});
next();
};
app.get("/settings/profile", requireLogin, (req, res) => {
const user = getUserStmt.get(req.session.user);
if (!user) return res.status(404).json({error: "User not found"});
res.json({username: user.username});
});
app.put("/settings/username", requireLogin, async (req, res) => {
const currentUsername = req.session.user;
const nextUsername = (req.body?.username || "").trim();
const password = req.body?.password || "";
if (!nextUsername) {
return res.status(400).json({error: "New username is required"});
}
if (nextUsername.length < 3) {
return res.status(400).json({error: "Username must be at least 3 characters"});
}
if (nextUsername === currentUsername) {
return res.status(400).json({error: "That is already your username"});
}
const currentUser = getUserStmt.get(currentUsername);
if (!currentUser) {
return res.status(404).json({error: "User not found"});
}
const valid = await bcrypt.compare(password, currentUser.password);
if (!valid) {
return res.status(403).json({error: "Current password is incorrect"});
}
if (getUserStmt.get(nextUsername)) {
return res.status(400).json({error: "Username already exists"});
}
renameUserTransaction(currentUsername, nextUsername);
req.session.user = nextUsername;
res.json({username: nextUsername});
});
app.put("/settings/password", requireLogin, async (req, res) => {
const username = req.session.user;
const currentPassword = req.body?.currentPassword || "";
const nextPassword = req.body?.newPassword || "";
if (!currentPassword || !nextPassword) {
return res.status(400).json({error: "Current and new password are required"});
}
if (nextPassword.length < 6) {
return res.status(400).json({error: "New password must be at least 6 characters"});
}
const user = getUserStmt.get(username);
if (!user) {
return res.status(404).json({error: "User not found"});
}
const valid = await bcrypt.compare(currentPassword, user.password);
if (!valid) {
return res.status(403).json({error: "Current password is incorrect"});
}
const hashed = await bcrypt.hash(nextPassword, 10);
updatePasswordStmt.run(hashed, username);
res.json({message: "Password updated"});
});
const normalizePersonaField = (value) => { const normalizePersonaField = (value) => {
const trimmed = (value || "").trim(); const trimmed = (value || "").trim();
return trimmed ? trimmed : null; return trimmed ? trimmed : null;
@@ -683,11 +820,6 @@ const buildUserPersonaPrompt = (persona) => {
return lines.join("\n"); return lines.join("\n");
}; };
const requireLogin = (req, res, next) => {
if (!req.session.user) return res.status(403).json({error: "Not logged in"});
next();
};
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) {
@@ -898,6 +1030,26 @@ app.get("/personas/market", requireLogin, (req, res) => {
res.json({personas}); res.json({personas});
}); });
app.get("/dashboard/summary", requireLogin, (req, res) => {
const user = req.session.user;
const chats = countChatSessionsStmt.get(user).count;
const messages = getChatMessageCountForUserStmt.get(user).count;
const personas = getPersonaCountForUserStmt.get(user).count;
const published = getPublishedPersonaCountForUserStmt.get(user).count;
const marketPersonas = listMarketPersonasStmt.all().length;
res.json({
username: user,
stats: {
chats,
messages,
personas,
published,
marketPersonas
}
});
});
app.post("/personas/:id/publish", requireLogin, (req, res) => { app.post("/personas/:id/publish", 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);
@@ -1090,6 +1242,14 @@ app.get("/chat/history", requireLogin, (req, res) => {
res.json({history: chats}); res.json({history: chats});
}); });
app.get("/market", (req, res) => {
res.sendFile(path.resolve("public/market.html"));
});
app.get("/settings", (req, res) => {
res.sendFile(path.resolve("public/settings.html"));
});
app.get("/", (req, res) => { app.get("/", (req, res) => {
res.sendFile(path.resolve("public/index.html")); res.sendFile(path.resolve("public/index.html"));
}); });