This commit is contained in:
Frederik Spirgi
2026-09-09 08:50:47 +02:00
parent da51bd88e6
commit 1ed0226b69
14 changed files with 80 additions and 57 deletions
+6 -2
View File
@@ -1,9 +1,13 @@
# KrisHD_AI
# Elsewhere
KrisHD_AI is an Express + SQLite app for account-based chat, persona management, roleplay chat sessions, and a simple persona market backed by a remote model API.
Elsewhere is an Express + SQLite app for account-based chat, persona management, roleplay chat sessions, and a simple persona market backed by a remote model API.
A space for every conversation. Six appearance styles (Grove, Parchment, Aurora, Current, Iris, and Graphite) each support light and dark modes. The Elsewhere identity stays consistent across styles. Internal theme and storage keys remain stable to preserve saved preferences.
## Setup
Node.js 22 or newer is required.
1. Install dependencies with `npm install`.
2. Copy `.env.example` to `.env` and set at least `SESSION_SECRET`.
3. Start the app with `npm run dev` for local development or `npm start` for a normal server run.
+5 -2
View File
@@ -3,6 +3,9 @@
"version": "1.0.0",
"type": "module",
"main": "server.js",
"engines": {
"node": ">=22"
},
"scripts": {
"start": "node server.js",
"dev": "node --watch server.js",
@@ -10,8 +13,8 @@
"test": "node --test"
},
"dependencies": {
"bcrypt": "^5.1.1",
"better-sqlite3": "^9.5.0",
"bcrypt": "^6.0.0",
"better-sqlite3": "^13.0.3",
"body-parser": "^1.20.2",
"express": "^4.19.2",
"express-session": "^1.17.3",
+7 -6
View File
@@ -6,11 +6,12 @@ export const onboardingPrompts = {
roleplay: ["Start a roleplay scene with immediate tension.", "Give me a dramatic opening with clear stakes."]
};
// Stable keys preserve existing saved appearance preferences.
export const themes = {
"fakegpt": {name: "FakeGPT", short: "FG"},
"fraud": {name: "Fraud", short: "FR"},
"germini": {name: "Germini", short: "GE"},
"slopilot": {name: "Slopilot", short: "SP"},
"beta-ai": {name: "Beta AI", short: "BA"},
"confusity": {name: "Confusity", short: "CF"}
"fakegpt": {name: "Grove", short: "GR"},
"fraud": {name: "Parchment", short: "PA"},
"germini": {name: "Aurora", short: "AU"},
"slopilot": {name: "Current", short: "CU"},
"beta-ai": {name: "Iris", short: "IR"},
"confusity": {name: "Graphite", short: "GP"}
};
+4 -4
View File
@@ -112,8 +112,8 @@ const onboardingContinue = $("onboardingContinue");
const onboardingSkip = $("onboardingSkip");
const authScreens = document.querySelectorAll(".auth-screen");
const toggleButtons = document.querySelectorAll(".auth-toggle .toggle");
const themeNameTargets = document.querySelectorAll("[data-theme-name]");
const themeLogoTargets = document.querySelectorAll("[data-theme-logo]");
const brandNameTargets = document.querySelectorAll("[data-brand-name]");
const brandLogoTargets = document.querySelectorAll("[data-brand-logo]");
const modelSection = document.querySelector(".chat-drawer-section--model");
const personaSection = document.querySelector(".chat-drawer-section--persona");
@@ -231,8 +231,8 @@ export {
sendBtn,
saveGenerationSettingsBtn,
sessionUser,
themeLogoTargets,
themeNameTargets,
brandLogoTargets,
brandNameTargets,
toggleButtons,
userPersonaList
};
+6 -4
View File
@@ -24,16 +24,17 @@ export function applyTheme(themeKey, {
persist = true,
title = null,
themeSelect = null,
nameTargets = document.querySelectorAll("[data-theme-name]"),
logoTargets = document.querySelectorAll("[data-theme-logo]")
nameTargets = document.querySelectorAll("[data-brand-name]"),
logoTargets = document.querySelectorAll("[data-brand-logo]")
} = {}) {
const nextTheme = normalizeTheme(themeKey);
const theme = themes[nextTheme];
document.documentElement.dataset.theme = nextTheme;
document.body.dataset.theme = nextTheme;
if (typeof title === "function") document.title = title(theme);
if (typeof title === "string") document.title = title;
nameTargets.forEach((target) => { target.textContent = theme.name; });
logoTargets.forEach((target) => { target.textContent = theme.short; });
nameTargets.forEach((target) => { target.textContent = "elsewhere"; });
logoTargets.forEach((target) => { target.textContent = ""; target.classList.add("doorway-mark"); });
if (themeSelect) themeSelect.value = nextTheme;
if (persist) localStorage.setItem(THEME_STORAGE_KEY, nextTheme);
document.dispatchEvent(new CustomEvent("appearancechange", {detail: {theme: nextTheme}}));
@@ -42,6 +43,7 @@ export function applyTheme(themeKey, {
export function applyThemeMode(modeKey, {persist = true, modeSelect = null} = {}) {
const nextMode = normalizeThemeMode(modeKey);
document.documentElement.dataset.themeMode = nextMode;
document.body.dataset.themeMode = nextMode;
if (modeSelect) modeSelect.value = nextMode;
if (persist) localStorage.setItem(THEME_MODE_STORAGE_KEY, nextMode);
+10 -8
View File
@@ -2,12 +2,13 @@
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>FakeGPT</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<title>Elsewhere</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">
<script src="app/theme-bootstrap.js"></script>
<link rel="stylesheet" href="style.css?v=20260809-2"/>
<link rel="stylesheet" href="style.css?v=20260909-elsewhere"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body data-theme="fakegpt" data-theme-mode="light">
@@ -15,10 +16,11 @@
<section id="auth" class="card auth-card auth-card-compact">
<div class="auth-hero auth-hero-compact">
<div class="brand">
<div class="logo logo-placeholder" aria-hidden="true"></div>
<div class="logo doorway-mark" aria-hidden="true"></div>
<div>
<p class="eyebrow">Private workspace</p>
<h1 data-theme-name>FakeGPT</h1>
<p class="eyebrow">Your conversation space</p>
<h1 class="brand-wordmark" data-brand-name>elsewhere</h1>
<p class="brand-tagline">A space for every conversation.</p>
</div>
</div>
</div>
@@ -80,7 +82,7 @@
<div class="sidebar-workspace-card">
<div>
<p class="eyebrow">Workspace</p>
<h2 data-theme-name>FakeGPT</h2>
<h2 class="brand-wordmark" data-brand-name>elsewhere</h2>
<p id="sessionUser" class="sidebar-account-name">Signed out</p>
</div>
<div class="sidebar-header-actions sidebar-workspace-actions">
@@ -409,11 +411,11 @@
<div class="modal-header">
<div class="persona-modal-heading">
<p class="eyebrow">Get started</p>
<h3 id="onboardingTitle">What do you want to do?</h3>
<h3 id="onboardingTitle">Where shall we start?</h3>
</div>
<button id="onboardingSkip" type="button" class="icon-button">Skip</button>
</div>
<p id="onboardingSubtitle" class="subtitle">Pick one path to set up your first successful chat.</p>
<p id="onboardingSubtitle" class="subtitle">Ask a question, explore an idea, or step into a story.</p>
<div id="onboardingChoices" class="onboarding-choices"></div>
<div class="popup-actions">
<button id="onboardingBack" type="button" class="secondary-action hidden">Back</button>
+4 -3
View File
@@ -2,12 +2,13 @@
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>FakeGPT Market</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<title>Elsewhere 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">
<script src="app/theme-bootstrap.js"></script>
<link rel="stylesheet" href="style.css?v=20260809-2"/>
<link rel="stylesheet" href="style.css?v=20260909-elsewhere"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body data-theme="fakegpt" data-theme-mode="light">
@@ -15,7 +16,7 @@
<header class="market-header market-header-simple">
<div>
<p class="eyebrow">Shared personas</p>
<h1><span data-theme-name>FakeGPT</span> Market</h1>
<h1><span class="brand-wordmark" data-brand-name>elsewhere</span> Market</h1>
</div>
<div class="market-header-actions">
<button id="refreshMarket" class="secondary-action" type="button">Refresh</button>
+2 -2
View File
@@ -59,7 +59,7 @@ const marketPopupDescription = $("marketPopupDescription");
const marketPopupField = $("marketPopupField");
const marketPopupInputLabel = $("marketPopupInputLabel");
const marketPopupInput = $("marketPopupInput");
const themeNameTargets = document.querySelectorAll("[data-theme-name]");
const brandNameTargets = document.querySelectorAll("[data-brand-name]");
const marketPersonaFields = {
name: marketPersonaName,
pronouns: marketPersonaPronouns,
@@ -673,7 +673,7 @@ marketPersonaForm.addEventListener("submit", (event) => {
});
window.addEventListener("load", async () => {
initializeAppearance({title: (theme) => `${theme.name} Market`, nameTargets: themeNameTargets});
initializeAppearance({title: "Elsewhere Market", nameTargets: brandNameTargets});
const params = new URLSearchParams(window.location.search);
setMarketSort("assistant", params.get("assistantSort") || "best", {reload: false});
setMarketSort("user", params.get("userSort") || "best", {reload: false});
+1
View File
@@ -1,6 +1,7 @@
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Privacy Policy – DeepFake</title>
+8 -8
View File
@@ -111,8 +111,8 @@ import {
sendBtn,
saveGenerationSettingsBtn,
sessionUser,
themeLogoTargets,
themeNameTargets,
brandLogoTargets,
brandNameTargets,
toggleButtons,
userPersonaList
} from "./app/dom.js";
@@ -227,8 +227,8 @@ function renderOnboardingStep() {
onboardingBack.classList.toggle("hidden", onboardingStep === 1);
onboardingContinue.classList.toggle("hidden", onboardingStep === 2);
if (onboardingStep === 1) {
onboardingTitle.textContent = "What do you want to do?";
onboardingSubtitle.textContent = "Choose one path. You can switch anytime.";
onboardingTitle.textContent = "Where shall we start?";
onboardingSubtitle.textContent = "Ask a question, explore an idea, or step into a story.";
[["ask", "Ask questions"], ["brainstorm", "Brainstorm ideas"], ["roleplay", "Roleplay with a character"]].forEach(([key, label]) => {
const button = document.createElement("button");
button.type = "button";
@@ -812,7 +812,7 @@ function renderMessageEmptyState() {
actions.className = "messages-empty-actions";
if (!chat) {
title.textContent = "Start a chat";
title.textContent = "Where shall we start?";
description.textContent = "Choose a starting point.";
actions.append(
createStarterAction("New chat", "primary-action", () => { void createNewChat(); }),
@@ -951,9 +951,9 @@ function updateModelHelp() {
function applyTheme(themeKey, persist = true) {
applySharedTheme(themeKey, {
persist,
title: (theme) => theme.name,
nameTargets: themeNameTargets,
logoTargets: themeLogoTargets
title: "Elsewhere",
nameTargets: brandNameTargets,
logoTargets: brandLogoTargets
});
updateWorkspaceCopy();
renderModelSection();
+14 -13
View File
@@ -1,20 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<meta charset="UTF-8"/>
<title>Settings</title>
<title>Elsewhere 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">
<script src="app/theme-bootstrap.js"></script>
<link rel="stylesheet" href="style.css?v=20260809-2"/>
<link rel="stylesheet" href="style.css?v=20260909-elsewhere"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body data-theme="fakegpt" data-theme-mode="light">
<div class="settings-shell card">
<header class="market-header market-header-simple">
<div>
<p class="eyebrow">Settings</p>
<p class="eyebrow">Elsewhere settings</p>
<h1>Preferences</h1>
</div>
<div class="market-header-actions">
@@ -135,24 +136,24 @@
<div class="settings-section-header">
<div>
<p class="eyebrow">Appearance</p>
<h2>Theme</h2>
<p class="subtitle">Themes are only changed here.</p>
<h2>Make yourself at home</h2>
<p class="subtitle">Choose a style for your conversation space.</p>
</div>
</div>
<div class="settings-theme-row">
<label class="field">
<span>Theme</span>
<span>Style</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>
<option value="fakegpt">Grove</option>
<option value="fraud">Parchment</option>
<option value="germini">Aurora</option>
<option value="slopilot">Current</option>
<option value="beta-ai">Iris</option>
<option value="confusity">Graphite</option>
</select>
</label>
<label class="field">
<span>Theme mode</span>
<span>Light or dark</span>
<select id="themeModeSelect">
<option value="light">Light</option>
<option value="dark">Dark</option>
+4 -4
View File
@@ -127,7 +127,7 @@ function promptSettingsPopup({
}
function applyTheme(themeKey, persist = true) {
return applySharedTheme(themeKey, {persist, title: "Settings", themeSelect: settingsThemeSelect});
return applySharedTheme(themeKey, {persist, title: "Elsewhere Settings", themeSelect: settingsThemeSelect});
}
function applyThemeMode(modeKey, persist = true) {
@@ -526,11 +526,11 @@ passwordForm.addEventListener("submit", async (event) => {
settingsThemeSelect.addEventListener("change", (event) => {
applyTheme(event.target.value);
setNotice("Theme updated.", "success");
setNotice("Style updated.", "success");
});
themeModeSelect.addEventListener("change", (event) => {
applyThemeMode(event.target.value);
setNotice(`Theme mode set to ${document.body.dataset.themeMode}.`, "success");
setNotice(`Appearance set to ${document.body.dataset.themeMode}.`, "success");
});
workspaceModeSelect.addEventListener("change", (event) => {
const mode = event.target.value === "advanced" ? "advanced" : "basic";
@@ -572,7 +572,7 @@ settingsPopupInput.addEventListener("keydown", (event) => {
});
window.addEventListener("load", async () => {
initializeAppearance({title: "Settings", themeSelect: settingsThemeSelect, modeSelect: themeModeSelect});
initializeAppearance({title: "Elsewhere Settings", themeSelect: settingsThemeSelect, modeSelect: themeModeSelect});
workspaceModeSelect.value = localStorage.getItem("krishd-workspace-mode") || "basic";
const params = new URLSearchParams(window.location.search);
const view = params.get("view") || "personal";
+8 -1
View File
@@ -1,4 +1,4 @@
/* KrisHD AI design system — rebuilt from scratch. */
/* Elsewhere design system. */
/* ---------- Foundation ---------- */
:root {
@@ -611,3 +611,10 @@ input:focus, textarea:focus, select:focus { border-color: var(--brand); }
@keyframes loading-bar { 0%, 100% { height: 7px; opacity: .45; } 50% { height: 18px; opacity: 1; } }
@keyframes pulse { 50% { opacity: .35; } }
@keyframes wave { 50% { height: 28px; } }
/* Permanent Elsewhere identity, shared by every appearance style. */
.brand-wordmark { font-family: "Manrope", "Segoe UI", sans-serif; font-weight: 700; letter-spacing: -.055em; }
.brand-tagline { margin: 12px 0 0; color: var(--text-soft); font-size: .95rem; }
.doorway-mark { position: relative; flex-shrink: 0; }
.doorway-mark::before { content: ""; width: 22px; height: 30px; border: 2px solid currentColor; border-bottom: 0; border-radius: 10px 10px 0 0; }
.doorway-mark::after { content: ""; position: absolute; width: 13px; height: 25px; left: calc(50% - 10px); top: calc(50% - 8px); border: 2px solid currentColor; background: var(--brand); transform: skewY(-18deg); transform-origin: left top; }
+1
View File
@@ -1,6 +1,7 @@
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Terms of Service – DeepFake</title>