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">
<head>
<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"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div class="app-shell">
<div id="auth" class="card">
<div class="brand">
<div class="logo">DF</div>
<div>
<p class="eyebrow">Best AI chat in the big 2026</p>
<h1>Welcome to DeepFake</h1>
<p class="subtitle">Log in to continue your conversation or create an account to get started.</p>
<section id="auth" class="card auth-card auth-card-compact">
<div class="auth-hero auth-hero-compact">
<div class="brand">
<div class="logo" data-theme-logo>FG</div>
<div>
<p class="eyebrow">Private chat</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 class="auth-toggle">
<button class="toggle active" data-target="login">Login</button>
<button class="toggle" data-target="register">Register</button>
</div>
<div class="auth-panel">
<div class="auth-toggle">
<button class="toggle active" data-target="login">Login</button>
<button class="toggle" data-target="register">Register</button>
</div>
<div id="loginScreen" class="auth-screen active">
<form id="loginForm" class="auth-form">
<label class="field">
<span>Username</span>
<input id="loginUsername" placeholder="Enter your username" autocomplete="username"/>
</label>
<label class="field">
<span>Password</span>
<input id="loginPassword" type="password" placeholder="Enter your password"
autocomplete="current-password"/>
</label>
<button id="loginSubmit" type="submit" class="primary-action">Sign in</button>
</form>
</div>
<div id="loginScreen" class="auth-screen active">
<form id="loginForm" class="auth-form">
<label class="field">
<span>Username</span>
<input id="loginUsername" placeholder="Enter your username" autocomplete="username"/>
</label>
<label class="field">
<span>Password</span>
<input id="loginPassword" type="password" placeholder="Enter your password" autocomplete="current-password"/>
</label>
<button id="loginSubmit" type="submit" class="primary-action">Sign in</button>
</form>
</div>
<div id="registerScreen" class="auth-screen">
<form id="registerForm" class="auth-form">
<label class="field">
<span>Create username</span>
<input id="registerUsername" placeholder="Choose a username" autocomplete="new-username"/>
</label>
<label class="field">
<span>Create password</span>
<input id="registerPassword" type="password" placeholder="Create a password"
autocomplete="new-password"/>
</label>
<div class="consent-container">
<input type="checkbox" id="consentCheckbox" required/>
<label for="consentCheckbox">
I agree to the
<a href="/tos.html" target="_blank">Terms of Service</a> and
<a href="/privacy.html" target="_blank">Privacy Policy</a>.
<div id="registerScreen" class="auth-screen">
<form id="registerForm" class="auth-form">
<label class="field">
<span>Create username</span>
<input id="registerUsername" placeholder="Choose a username" autocomplete="new-username"/>
</label>
<label class="field">
<span>Create password</span>
<input id="registerPassword" type="password" placeholder="Create a password" autocomplete="new-password"/>
</label>
<div class="consent-container">
<input type="checkbox" id="consentCheckbox" required/>
<label for="consentCheckbox">
I agree to the
<a href="/tos.html" target="_blank">Terms of Service</a> and
<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>
</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 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>
<section class="chat-main">
<div class="chat-header">
<div class="chat-title">
<h2 id="activeChatTitle">New chat</h2>
<button id="renameChat" class="icon-button" type="button">Rename</button>
<section class="chat-main chat-main-focus">
<header class="chat-header chat-header-focus">
<div class="chat-title-block">
<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 class="chat-header-actions">
<div class="model-select">
<label for="model">Model:</label>
<label for="model">Model</label>
<select id="model">
<option disabled selected>Loading models...</option>
</select>
</div>
<a href="/market.html" class="secondary-action market-link">Persona Market</a>
<div class="persona-menu">
<button id="personaMenuButton" class="secondary-action" type="button"
aria-expanded="false" aria-controls="personaPopover">
<button id="personaMenuButton" class="secondary-action" type="button" aria-expanded="false" aria-controls="personaPopover">
Personas
</button>
<div id="personaPopover" class="persona-popover hidden" role="dialog">
<div class="persona-section">
<div class="persona-section-header">
<div>
<p class="eyebrow">Roleplay</p>
<h2>AI Persona</h2>
<p class="persona-guidance">Controls how the assistant sounds and behaves.</p>
<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 persona equipped.</p>
<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>
<p class="eyebrow">Roleplay</p>
<h2>Your Persona</h2>
<p class="persona-guidance">Defines how the assistant should portray you.</p>
<p class="eyebrow">User</p>
<h2>Your persona</h2>
<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>
@@ -130,20 +159,30 @@
</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 id="messages"></div>
<div class="input-container">
<textarea id="msgInput" placeholder="Type your message..."></textarea>
<button id="send"></button>
</div>
<div class="logout-container">
<button id="deleteChat" class="danger">Delete Chat</button>
<button id="logout">Logout</button>
<div class="input-shell input-shell-focus">
<div class="input-meta">
<p class="composer-hint">Enter sends. Shift+Enter adds a new line.</p>
</div>
<div class="input-container">
<textarea id="msgInput" placeholder="Type your message..."></textarea>
<button id="send" type="button" aria-label="Send message"></button>
</div>
</div>
</section>
</div>
</div>
</section>
</div>
<div id="personaModal" class="modal-overlay hidden">
<div class="modal-card">
<div class="modal-header">
@@ -158,7 +197,7 @@
<option value="user">Your persona</option>
</select>
</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">
<span>Name</span>
<input id="personaName" placeholder="Persona name" required/>
@@ -177,8 +216,7 @@
</label>
<label class="field">
<span>Details</span>
<textarea id="personaDetails" rows="3"
placeholder="Personality traits, voice, or quirks"></textarea>
<textarea id="personaDetails" rows="3" placeholder="Personality traits, voice, quirks, goals"></textarea>
</label>
<div class="persona-actions">
<button id="personaSave" type="submit" class="primary-action">Save persona</button>
@@ -186,6 +224,7 @@
</form>
</div>
</div>
<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="script.js" defer></script>
+19 -13
View File
@@ -2,41 +2,47 @@
<html lang="en">
<head>
<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"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div class="market-shell card">
<header class="market-header">
<div class="market-shell card market-shell-simple">
<header class="market-header market-header-simple">
<div>
<p class="eyebrow">Community</p>
<h1>Persona Market</h1>
<p class="subtitle">Browse shared personas and choose who should embody them.</p>
<div class="market-search">
<label for="marketSearch">Search personas</label>
<input id="marketSearch" type="search" placeholder="Search by name, creator, or details"/>
</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>
</div>
<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>
</div>
</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>
<div class="persona-market-columns market-page-columns">
<div class="persona-market-column">
<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>
<div class="persona-market-column">
<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>
</div>
</div>
<div id="marketPreviewModal" class="modal-overlay hidden">
<div class="modal-card market-preview-card">
<div class="modal-header">
+108 -176
View File
@@ -1,114 +1,117 @@
const marketPersonaList = document.getElementById("marketPersonaList");
const marketUserPersonaList = document.getElementById("marketUserPersonaList");
const refreshMarketBtn = document.getElementById("refreshMarket");
const marketStatus = document.getElementById("marketStatus");
const marketSearchInput = document.getElementById("marketSearch");
const marketPreviewModal = document.getElementById("marketPreviewModal");
const marketPreviewName = document.getElementById("marketPreviewName");
const marketPreviewMeta = document.getElementById("marketPreviewMeta");
const marketPreviewPronouns = document.getElementById("marketPreviewPronouns");
const marketPreviewAppearance = document.getElementById("marketPreviewAppearance");
const marketPreviewBackground = document.getElementById("marketPreviewBackground");
const marketPreviewDetails = document.getElementById("marketPreviewDetails");
const marketPreviewConfirm = document.getElementById("marketPreviewConfirm");
const marketPreviewCancel = document.getElementById("marketPreviewCancel");
const marketPreviewClose = document.getElementById("marketPreviewClose");
let allMarketPersonas = [];
let pendingMarketPersona = null;
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 themeNameTargets = document.querySelectorAll("[data-theme-name]");
let allMarketPersonas = [], pendingMarketPersona = null;
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
});
const res = await fetch(url, {method, headers: {"Content-Type": "application/json"}, body: data ? JSON.stringify(data) : undefined});
return await res.json();
} catch (err) {
return {error: "Network error — please try again."};
} catch {
return {error: "Network error - please try again."};
}
}
async function post(url, data) {
return request(url, data, "POST");
}
const get = (url) => request(url, null, "GET");
const post = (url, data) => request(url, data, "POST");
async function get(url) {
try {
const res = await fetch(url);
return await res.json();
} catch (err) {
return {error: "Network error — please try again."};
}
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; });
}
function setMarketStatus(message, state = "") {
if (!marketStatus) return;
marketStatus.textContent = message;
marketStatus.className = state ? `status persona-status ${state}` : "status persona-status";
}
function buildPersonaMeta(persona) {
const pronouns = persona.pronouns ? `Pronouns: ${persona.pronouns}` : "Pronouns: —";
const usageCount = Number(persona.usage_count || 0);
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);
});
const pronouns = persona.pronouns ? `Pronouns: ${persona.pronouns}` : "Pronouns: n/a";
return `${persona.creator_username} | ${pronouns}`;
}
function formatPersonaField(value) {
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) {
if (!marketPreviewModal) return;
pendingMarketPersona = persona;
marketPreviewName.textContent = persona.name;
marketPreviewMeta.textContent = buildPersonaMeta(persona);
@@ -116,110 +119,39 @@ function openMarketPreview(persona) {
marketPreviewAppearance.textContent = formatPersonaField(persona.appearance);
marketPreviewBackground.textContent = formatPersonaField(persona.background);
marketPreviewDetails.textContent = formatPersonaField(persona.details);
marketPreviewConfirm.textContent =
persona.persona_type === "assistant" ? "Get & equip as AI" : "Get & equip as You";
marketPreviewConfirm.textContent = persona.persona_type === "assistant" ? "Collect as AI persona" : "Collect as user persona";
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() {
setMarketStatus("Loading market personas...");
const res = await get("/personas/market");
if (res.error) {
setMarketStatus(res.error, "error");
return;
}
if (res.error) return setMarketStatus(res.error, "error");
allMarketPersonas = res.personas || [];
applyMarketFilter();
}
async function collectMarketPersona(marketId, personaType) {
setMarketStatus(
personaType === "assistant"
? "Equipping the AI persona..."
: "Equipping the user persona..."
);
setMarketStatus(personaType === "assistant" ? "Collecting AI persona..." : "Collecting user persona...");
const res = await post(`/personas/market/${marketId}/collect`, {equip: true});
if (res.error) {
setMarketStatus(res.error, "error");
return;
}
if (res.error) return setMarketStatus(res.error, "error");
await loadMarketPersonas();
setMarketStatus(
personaType === "assistant"
? "AI persona equipped. Head back to chat to use it."
: "User persona equipped. Head back to chat to use it.",
"success"
);
setMarketStatus(personaType === "assistant" ? "AI persona collected." : "User persona collected.", "success");
}
if (refreshMarketBtn) {
refreshMarketBtn.addEventListener("click", () => loadMarketPersonas());
}
if (marketSearchInput) {
marketSearchInput.addEventListener("input", () => applyMarketFilter());
}
if (marketPreviewCancel) {
marketPreviewCancel.addEventListener("click", closeMarketPreview);
}
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);
});
}
refreshMarketBtn.addEventListener("click", loadMarketPersonas);
marketSearchInput.addEventListener("input", applyMarketFilter);
marketPreviewCancel.addEventListener("click", closeMarketPreview);
marketPreviewClose.addEventListener("click", closeMarketPreview);
marketPreviewModal.addEventListener("click", (event) => { if (event.target === marketPreviewModal) closeMarketPreview(); });
marketPreviewConfirm.addEventListener("click", async () => {
if (!pendingMarketPersona) return;
const {id, persona_type} = pendingMarketPersona;
closeMarketPreview();
await collectMarketPersona(id, persona_type);
});
window.addEventListener("load", () => {
applyTheme(localStorage.getItem("krishd-theme") || "fakegpt");
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