Overhaul legal page and global styles for consistency and responsiveness.

This commit is contained in:
2026-08-09 18:14:13 +02:00
parent 20b5129f57
commit cb330b332e
18 changed files with 1055 additions and 6802 deletions
+15
View File
@@ -58,6 +58,20 @@ function toggleChatDrawer() {
syncChatDrawerState(!isChatDrawerOpen());
}
function handleDisclosureToggle(event) {
const disclosure = event.target;
if (!(disclosure instanceof HTMLDetailsElement) || !chatDrawer?.contains(disclosure)) return;
if (disclosure.matches(".chat-drawer-more, .chat-drawer-danger")) {
const hint = disclosure.querySelector(":scope > summary .chat-drawer-row-hint");
if (hint) hint.textContent = disclosure.open ? "Hide" : "Show";
}
if (disclosure.open) {
chatDrawer.querySelectorAll("details[open]").forEach((other) => {
if (other !== disclosure) other.open = false;
});
}
}
function handleDocumentClick(event) {
if (!isChatDrawerOpen()) return;
const target = event.target;
@@ -103,6 +117,7 @@ if (chatDrawerCloseBtn) {
document.addEventListener("click", handleDocumentClick);
document.addEventListener("keydown", handleDocumentKeydown);
chatDrawer?.addEventListener("toggle", handleDisclosureToggle, true);
export {
closeChatDrawer,
+6
View File
@@ -8,6 +8,8 @@ const messagesDiv = $("messages");
const modelSelect = $("model");
const modelPanel = $("modelPanel");
const modelBadgeName = $("modelBadgeName");
const headerModelName = $("headerModelName");
const headerPersonaName = $("headerPersonaName");
const modelCount = $("modelCount");
const modelHelpTitle = $("modelHelpTitle");
const modelHelpBadge = $("modelHelpBadge");
@@ -41,6 +43,7 @@ const exportChatBtn = $("exportChat");
const pinChatBtn = $("pinChat");
const moveChatFolderBtn = $("moveChatFolder");
const manageChatMemoryBtn = $("manageChatMemory");
const managePersonaStateBtn = $("managePersonaState");
const archiveChatBtn = $("archiveChat");
const backupWorkspaceBtn = $("backupWorkspace");
const importWorkspaceBtn = $("importWorkspace");
@@ -139,6 +142,7 @@ export {
pinChatBtn,
moveChatFolderBtn,
manageChatMemoryBtn,
managePersonaStateBtn,
archiveChatBtn,
backupWorkspaceBtn,
importWorkspaceBtn,
@@ -150,6 +154,8 @@ export {
logoutButton,
messagesDiv,
modelBadgeName,
headerModelName,
headerPersonaName,
modelCount,
modelHelpBadge,
modelHelpSummary,
+21 -16
View File
@@ -6,10 +6,11 @@
<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"/>
<script src="app/theme-bootstrap.js"></script>
<link rel="stylesheet" href="style.css?v=20260809-2"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<body data-theme="fakegpt" data-theme-mode="light">
<div class="app-shell">
<section id="auth" class="card auth-card auth-card-compact">
<div class="auth-hero auth-hero-compact">
@@ -72,7 +73,7 @@
</div>
</section>
<section id="chat" class="card chat-card chat-focus-card" aria-label="AI chat workspace" style="display:none;">
<section id="chat" class="card chat-card chat-focus-card hidden" aria-label="AI chat workspace">
<div class="chat-layout chat-layout-focus">
<aside id="chatSidebar" class="chat-sidebar" aria-label="Workspace navigation">
<div class="sidebar-top">
@@ -111,24 +112,27 @@
<div class="chat-title">
<h2 id="activeChatTitle">New chat</h2>
</div>
<div class="chat-header-context" aria-label="Current chat setup">
<span class="chat-context-item"><span>Model</span><strong id="headerModelName">Loading...</strong></span>
<span class="chat-context-item"><span>Persona</span><strong id="headerPersonaName">None</strong></span>
</div>
</div>
<div class="chat-header-actions">
<div class="chat-actions-menu">
<button id="chatDrawerButton" class="secondary-action overflow-trigger" type="button" aria-expanded="false" aria-controls="chatDrawer">
<span class="overflow-trigger-label">Controls</span>
<span class="overflow-trigger-label">Chat settings</span>
</button>
<div id="chatDrawer" class="chat-drawer hidden" role="dialog" aria-modal="true" aria-labelledby="chatDrawerTitle">
<div class="chat-drawer-panel">
<div class="chat-drawer-header">
<div>
<p class="eyebrow">Conversation</p>
<h2 id="chatDrawerTitle">Chat controls</h2>
<h2 id="chatDrawerTitle">Settings</h2>
</div>
<button id="chatDrawerClose" class="icon-button" type="button" aria-label="Close panel">Close</button>
<button id="chatDrawerClose" class="icon-button" type="button" aria-label="Close chat settings">×</button>
</div>
<div class="chat-drawer-body">
<div class="chat-drawer-summary">
<p class="dropdown-section-label">Current mode</p>
<div class="chat-drawer-pills">
<span id="chatCharacterPill" class="context-pill hidden">Character: none</span>
<span id="chatUserPersonaPill" class="context-pill hidden">User persona: none</span>
@@ -137,8 +141,8 @@
<details class="chat-drawer-section chat-drawer-disclosure chat-drawer-section--model">
<summary class="chat-drawer-row">
<span class="chat-drawer-row-label">
<span class="eyebrow">Model</span>
<strong>Engine</strong>
<span class="eyebrow">AI model</span>
<strong>Model</strong>
</span>
<span class="chat-drawer-row-meta">
<strong id="modelBadgeName">Loading...</strong>
@@ -170,8 +174,8 @@
<details class="chat-drawer-section chat-drawer-disclosure chat-drawer-section--persona">
<summary class="chat-drawer-row">
<span class="chat-drawer-row-label">
<span class="eyebrow">Persona</span>
<strong>Identity</strong>
<span class="eyebrow">Your identity</span>
<strong>Persona</strong>
</span>
<span class="chat-drawer-row-meta">
<strong id="chatDrawerPersonaValue">None</strong>
@@ -195,7 +199,7 @@
</div>
</details>
<div class="chat-drawer-section">
<p class="dropdown-section-label">Current chat</p>
<p class="dropdown-section-label">Chat</p>
<div class="chat-drawer-action-list">
<button id="renameChat" class="secondary-action chat-drawer-action" type="button" data-chat-action="rename">Rename chat</button>
<button id="pinChat" class="secondary-action chat-drawer-action" type="button" data-chat-action="pin">Pin</button>
@@ -206,13 +210,14 @@
<details class="chat-drawer-section chat-drawer-more">
<summary class="chat-drawer-row chat-drawer-row--compact">
<span class="chat-drawer-row-label">
<strong>More actions</strong>
<strong>Tools &amp; workspace</strong>
</span>
<span class="chat-drawer-row-hint">Expand</span>
<span class="chat-drawer-row-hint">Show</span>
</summary>
<div class="chat-drawer-action-list chat-drawer-disclosure-panel">
<button id="moveChatFolder" class="secondary-action chat-drawer-action" type="button" data-chat-action="folder">Move to folder</button>
<button id="manageChatMemory" class="secondary-action chat-drawer-action" type="button" data-chat-action="memory">Memory facts</button>
<button id="managePersonaState" class="secondary-action chat-drawer-action" type="button" data-chat-action="personaState">Roleplay state</button>
<button id="backupWorkspace" class="secondary-action chat-drawer-action" type="button">Backup workspace</button>
<button id="importWorkspace" class="secondary-action chat-drawer-action" type="button">Import workspace</button>
<input id="importWorkspaceFile" class="hidden" type="file" accept="application/json,.json"/>
@@ -222,9 +227,9 @@
<details class="chat-drawer-section chat-drawer-danger">
<summary class="chat-drawer-row chat-drawer-row--compact">
<span class="chat-drawer-row-label">
<strong>Danger zone</strong>
<strong>Clear conversation</strong>
</span>
<span class="chat-drawer-row-hint">Expand</span>
<span class="chat-drawer-row-hint">Show</span>
</summary>
<div class="chat-drawer-action-list chat-drawer-disclosure-panel">
<button id="clearChat" class="secondary-action chat-drawer-action chat-drawer-action-danger" type="button" data-chat-action="clear">Clear chat</button>
+17 -45
View File
@@ -1,49 +1,21 @@
body.legal-page {
display: block;
height: auto;
background-attachment: fixed;
background-repeat: no-repeat;
background-size: cover;
min-height: 100%;
overflow-y: auto;
overflow-x: hidden;
padding: 0;
.legal-page {
min-height: calc(100dvh - 36px);
display: grid;
place-items: center;
}
.legal-container,
.legal-content {
max-width: 900px;
margin: 0 auto;
padding: 2rem 1.5rem;
color: var(--text-primary);
font-family: inherit;
width: min(860px, 100%);
padding: clamp(24px, 5vw, 52px);
background: var(--surface-raised);
border: 1px solid var(--border);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-md);
}
.legal-content h1,
.legal-content h2 {
color: var(--text-primary);
}
.legal-content p,
.legal-content li {
line-height: 1.6;
color: var(--text-secondary);
}
.legal-content * {
overflow-wrap: break-word;
word-break: break-word;
}
@media (max-width: 600px) {
.legal-content {
padding: 1.25rem 1rem;
}
.legal-content h1 {
font-size: 1.6rem;
}
.legal-content h2 {
font-size: 1.2rem;
}
.legal-container h1, .legal-content h1 { margin-bottom: 20px; }
.legal-container h2, .legal-content h2 { margin: 28px 0 10px; font-size: 1.25rem; }
.legal-container p, .legal-container li, .legal-content p, .legal-content li { color: var(--text-soft); line-height: 1.75; }
.legal-container a, .legal-content a { font-weight: 700; }
@media (max-width: 700px) {
.legal-container, .legal-content { min-height: 100dvh; border: 0; border-radius: 0; }
}
+4 -3
View File
@@ -6,10 +6,11 @@
<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"/>
<script src="app/theme-bootstrap.js"></script>
<link rel="stylesheet" href="style.css?v=20260809-2"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<body data-theme="fakegpt" data-theme-mode="light">
<div class="market-shell card market-shell-simple">
<header class="market-header market-header-simple">
<div>
@@ -237,6 +238,6 @@
</div>
</div>
</div>
<script src="market.js" defer></script>
<script type="module" src="market.js"></script>
</body>
</html>
+18 -59
View File
@@ -1,3 +1,8 @@
import {get, post, put} from "./app/api.js";
import {initializeAppearance} from "./app/themeController.js";
import {clearStatus, setStatus} from "./app/statusNotice.js";
import {populatePersonaForm, readPersonaForm} from "./app/personaForm.js";
const $ = (id) => document.getElementById(id);
const marketPersonaList = $("marketPersonaList");
const marketUserPersonaList = $("marketUserPersonaList");
@@ -55,14 +60,13 @@ const marketPopupField = $("marketPopupField");
const marketPopupInputLabel = $("marketPopupInputLabel");
const marketPopupInput = $("marketPopupInput");
const themeNameTargets = document.querySelectorAll("[data-theme-name]");
const themes = {
"fakegpt": {name: "FakeGPT"},
"fraud": {name: "Fraud"},
"germini": {name: "Germini"},
"slopilot": {name: "Slopilot"},
"beta-ai": {name: "Beta AI"},
"confusity": {name: "Confusity"}
const marketPersonaFields = {
name: marketPersonaName,
pronouns: marketPersonaPronouns,
appearance: marketPersonaAppearance,
background: marketPersonaBackground,
details: marketPersonaDetails,
exampleDialogues: marketPersonaExampleDialogues
};
const validMarketSorts = new Set(["best", "newest", "most_favorited", "most_popular", "top_rated", "alphabetical"]);
@@ -78,40 +82,8 @@ let marketActivityDepth = 0;
let marketPreviewStep = 1;
let marketPopupResolver = null;
async function request(url, data, method = "POST") {
try {
const res = await fetch(url, {
method,
credentials: "same-origin",
headers: {"Content-Type": "application/json"},
body: data ? JSON.stringify(data) : undefined
});
return await res.json();
} catch {
return {error: "Network error - please try again."};
}
}
const get = (url) => request(url, null, "GET");
const post = (url, data) => request(url, data, "POST");
const put = (url, data) => request(url, data, "PUT");
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 applyThemeMode(modeKey) {
document.body.dataset.themeMode = modeKey === "dark" ? "dark" : "light";
}
function setMarketStatus(message, state = "") {
marketStatus.textContent = message;
marketStatus.className = state ? `status persona-status ${state}` : "status persona-status";
setStatus(marketStatus, message, state, "status persona-status");
}
function closeMarketPopup(value = null) {
@@ -150,12 +122,10 @@ function promptMarketPopup({
function setPersonaFormNotice(message = "", state = "") {
if (!message) {
marketPersonaFormNotice.textContent = "";
marketPersonaFormNotice.className = "status hidden";
clearStatus(marketPersonaFormNotice);
return;
}
marketPersonaFormNotice.textContent = message;
marketPersonaFormNotice.className = state ? `status ${state}` : "status";
setStatus(marketPersonaFormNotice, message, state);
}
function setMarketActivity(active, {
@@ -351,12 +321,7 @@ function openMarketPreview(persona) {
function openPersonaForm(persona = null) {
editingPersonaId = persona ? persona.id : null;
marketPersonaFormTitle.textContent = persona ? "Edit AI Character" : "Create AI Character";
marketPersonaName.value = persona?.name || "";
marketPersonaPronouns.value = persona?.pronouns || "";
marketPersonaAppearance.value = persona?.appearance || "";
marketPersonaBackground.value = persona?.background || "";
marketPersonaDetails.value = persona?.details || "";
marketPersonaExampleDialogues.value = persona?.example_dialogues || "";
populatePersonaForm(marketPersonaFields, persona || {});
setPersonaFormNotice("");
marketPersonaModal.classList.remove("hidden");
marketPersonaName.focus();
@@ -600,13 +565,8 @@ async function startMarketPersonaChat(marketId, userPersonaSelection, scenarioPr
async function savePersona() {
const payload = {
...readPersonaForm(marketPersonaFields),
personaType: "assistant",
name: marketPersonaName.value.trim(),
pronouns: marketPersonaPronouns.value.trim(),
appearance: marketPersonaAppearance.value.trim(),
background: marketPersonaBackground.value.trim(),
details: marketPersonaDetails.value.trim(),
exampleDialogues: marketPersonaExampleDialogues.value.trim()
};
if (!payload.name) {
@@ -713,8 +673,7 @@ marketPersonaForm.addEventListener("submit", (event) => {
});
window.addEventListener("load", async () => {
applyTheme(localStorage.getItem("krishd-theme") || "fakegpt");
applyThemeMode(localStorage.getItem("krishd-theme-mode") || "light");
initializeAppearance({title: (theme) => `${theme.name} Market`, nameTargets: themeNameTargets});
const params = new URLSearchParams(window.location.search);
setMarketSort("assistant", params.get("assistantSort") || "best", {reload: false});
setMarketSort("user", params.get("userSort") || "best", {reload: false});
+96 -26
View File
@@ -1,6 +1,7 @@
import {del, get, post, put, stream} from "./app/api.js";
import {closeChatDrawer} from "./app/chatDrawer.js";
import {defaultModelProfile, onboardingPrompts, requestedChatId, themes} from "./app/constants.js";
import {defaultModelProfile, onboardingPrompts, requestedChatId} from "./app/constants.js";
import {applyTheme as applySharedTheme, applyThemeMode as applySharedThemeMode, readStoredAppearance} from "./app/themeController.js";
import {
activeChatTitle,
activeUserPersonaStatus,
@@ -28,7 +29,10 @@ import {
exportChatBtn,
importWorkspaceBtn,
manageChatMemoryBtn,
managePersonaStateBtn,
importWorkspaceFileInput,
headerModelName,
headerPersonaName,
moveChatFolderBtn,
loginForm,
loginPasswordInput,
@@ -559,6 +563,7 @@ function addMessage(contentOrMessage, isUser = false, isLoading = false, options
if (isLoading) {
msgDiv.setAttribute("aria-label", "Assistant is responding");
msgDiv.innerHTML = `
<div class="msg-header">Assistant</div>
<div class="loading-shell" aria-live="polite" aria-label="Assistant is responding">
<div class="loading-bars">
<span class="loading-bar"></span>
@@ -569,7 +574,8 @@ function addMessage(contentOrMessage, isUser = false, isLoading = false, options
<div class="loading-copy">
<strong>Thinking</strong>
</div>
</div>`;
</div>
<div class="msg-content streaming-content"></div>`;
} else {
const message = typeof contentOrMessage === "object" && contentOrMessage
? contentOrMessage
@@ -728,7 +734,13 @@ function renderMessageEmptyState() {
createStarterAction("Change setup", "secondary-action", () => openRoleplayStarter())
);
} else {
return;
title.textContent = "What can I help with?";
description.textContent = "Write a message below or start with one of these prompts.";
actions.append(
createMessageStarterPrompt("Explain something", "Explain this topic in simple terms: "),
createMessageStarterPrompt("Plan a task", "Help me make a practical step-by-step plan for: "),
createMessageStarterPrompt("Brainstorm ideas", "Give me several creative ideas for: ")
);
}
empty.append(title, description, actions);
@@ -849,6 +861,7 @@ function updateModelHelp() {
if (!selectedOption || selectedOption.disabled) {
if (modelCount) modelCount.textContent = "No models";
if (modelBadgeName) modelBadgeName.textContent = "Unavailable";
if (headerModelName) headerModelName.textContent = "Unavailable";
modelHelpBadge.dataset.badge = "waiting";
modelHelpTitle.textContent = "Choose a model";
modelHelpBadge.textContent = "Waiting";
@@ -863,6 +876,7 @@ function updateModelHelp() {
const modelName = selectedOption.textContent || modelId;
const profile = getModelProfile(modelId, modelName);
if (modelBadgeName) modelBadgeName.textContent = modelName;
if (headerModelName) headerModelName.textContent = modelName;
modelHelpTitle.textContent = modelName;
modelHelpBadge.textContent = profile.badge;
modelHelpBadge.dataset.badge = profile.badge.toLowerCase();
@@ -870,21 +884,18 @@ function updateModelHelp() {
}
function applyTheme(themeKey, persist = true) {
const nextTheme = themes[themeKey] ? themeKey : "fakegpt";
const theme = themes[nextTheme];
document.body.dataset.theme = nextTheme;
document.title = theme.name;
themeNameTargets.forEach((target) => { target.textContent = theme.name; });
themeLogoTargets.forEach((target) => { target.textContent = theme.short; });
if (persist) localStorage.setItem("krishd-theme", nextTheme);
applySharedTheme(themeKey, {
persist,
title: (theme) => theme.name,
nameTargets: themeNameTargets,
logoTargets: themeLogoTargets
});
updateWorkspaceCopy();
renderModelSection();
}
function applyThemeMode(modeKey, persist = true) {
const nextMode = modeKey === "dark" ? "dark" : "light";
document.body.dataset.themeMode = nextMode;
if (persist) localStorage.setItem("krishd-theme-mode", nextMode);
applySharedThemeMode(modeKey, {persist});
}
function updateChatActionState() {
@@ -897,6 +908,7 @@ function updateChatActionState() {
}
if (moveChatFolderBtn) moveChatFolderBtn.disabled = !hasChat;
if (manageChatMemoryBtn) manageChatMemoryBtn.disabled = !hasChat;
if (managePersonaStateBtn) managePersonaStateBtn.disabled = !hasChat || !activeChat?.assistant_persona_id;
if (archiveChatBtn) {
archiveChatBtn.disabled = !hasChat;
if (hasChat) archiveChatBtn.textContent = activeChat.archived_at ? "Unarchive" : "Archive";
@@ -912,6 +924,7 @@ function renderPersonaSection() {
if (chatDrawerPersonaValue) {
const active = userPersonas.find((persona) => persona.id === activeUserPersonaId);
chatDrawerPersonaValue.textContent = active ? active.name : "None";
if (headerPersonaName) headerPersonaName.textContent = active ? active.name : "None";
}
if (clearUserPersonaBtn) clearUserPersonaBtn.disabled = !activeUserPersonaId;
}
@@ -1280,7 +1293,11 @@ function setLoadingState(loading, overlayOptions = null) {
sendBtn.classList.toggle("loading", loading);
if (loading) {
msgInput.placeholder = "Processing response...";
setChatActivity(true, overlayOptions || undefined);
if (overlayOptions === false) {
setChatActivity(false);
} else {
setChatActivity(true, overlayOptions || undefined);
}
} else {
setChatActivity(false);
updateComposerPlaceholder();
@@ -1590,6 +1607,54 @@ async function manageChatMemory() {
return setNotice("Memory fact saved.", "success");
}
const personaStateFields = [
["relationship_notes", "Relationship notes"],
["tone", "Tone"],
["current_location", "Current location"],
["goals", "Goals"],
["unresolved_threads", "Unresolved threads"],
["boundaries", "Boundaries"]
];
function formatPersonaState(state = {}) {
const lines = personaStateFields.map(([key, label], index) => {
const value = String(state[key] || "").trim();
return `${index + 1}. ${label}: ${value || "Not set"}`;
});
return lines.join("\n");
}
async function managePersonaState() {
if (!activeChatId) return;
const loaded = await get(`/chats/${activeChatId}/persona-state`);
if (loaded.error) return setNotice(loaded.error, "error");
const state = loaded.state || {};
const fieldChoice = await promptPopup({
eyebrow: "Roleplay",
title: "Roleplay state",
description: `${formatPersonaState(state)}\n\nType a field number to edit it.`,
label: "Field number",
placeholder: "1",
confirmLabel: "Next"
});
const selected = personaStateFields[Number(String(fieldChoice || "").trim()) - 1];
if (!selected) return;
const [key, label] = selected;
const value = await promptPopup({
eyebrow: "Roleplay",
title: label,
description: "Leave blank to clear this field.",
label,
value: state[key] || "",
placeholder: label
});
if (value === null) return;
const nextState = {...state, [key]: String(value || "").trim() || null};
const saved = await put(`/chats/${activeChatId}/persona-state`, nextState);
if (saved.error) return setNotice(saved.error, "error");
return setNotice("Roleplay state saved.", "success");
}
async function deleteChat(id) {
setChatLoading(true, "Deleting chat...");
const chat = getChatById(id);
@@ -1647,7 +1712,7 @@ async function checkSession() {
const res = await get("/session");
if (registerInviteField) registerInviteField.classList.toggle("hidden", !res.inviteOnlyRegistration);
if (!res.user) return false;
currentUsername = res.user; authDiv.style.display = "none"; chatDiv.style.display = "block";
currentUsername = res.user; authDiv.classList.add("hidden"); chatDiv.classList.remove("hidden");
await Promise.all([displayModels(), loadSummary(), loadChatSessions(), loadPersonas()]);
maybeShowOnboarding();
msgInput.focus(); return true;
@@ -1664,7 +1729,7 @@ async function handleAuth(endpoint, credentials) {
submitBtn.disabled = false;
if (res.error) return setAuthMessage(res.error, "error");
if (endpoint === "login") {
currentUsername = username; authDiv.style.display = "none"; chatDiv.style.display = "block";
currentUsername = username; authDiv.classList.add("hidden"); chatDiv.classList.remove("hidden");
await Promise.all([displayModels(), loadSummary(), loadChatSessions(), loadPersonas()]);
maybeShowOnboarding();
setNotice("Ready.", "success"); msgInput.focus();
@@ -1687,18 +1752,19 @@ async function sendMessage() {
currentMessages.push({role: "user", content: message}); addMessage(message, true); updateChatActionState();
msgInput.value = "";
resizeComposerInput();
setLoadingState(true, {
eyebrow: "Assistant replying",
title: "Generating response",
detail: "The assistant is reading your message and preparing a reply."
}); setNotice("Generating reply...");
const loadingMsg = addMessage({role: "bot", content: ""}, false, false, {isNewest: true});
setLoadingState(true, false); setNotice("Generating reply...");
const loadingMsg = addMessage({role: "bot", content: ""}, false, true, {isNewest: true});
const loadingBody = loadingMsg.querySelector(".msg-content");
const loadingShell = loadingMsg.querySelector(".loading-shell");
try {
let streamError = null;
await stream("/chat/stream", {message, model: modelSelect.value, chatId: activeChatId}, {
onChunk: ({fullReply}) => {
if (loadingBody) setMessageContent(loadingBody, fullReply);
if (loadingBody) {
loadingShell?.classList.add("hidden");
loadingBody.classList.add("has-content");
setMessageContent(loadingBody, fullReply);
}
},
onDone: () => {},
onError: ({error}) => {
@@ -1752,6 +1818,9 @@ const chatActions = {
memory: async () => {
await manageChatMemory();
},
personaState: async () => {
await managePersonaState();
},
pin: async () => {
const chat = getChatById(activeChatId);
if (chat) await updateChatOrganization(chat.id, {isPinned: !chat.is_pinned});
@@ -1776,7 +1845,7 @@ loginForm.addEventListener("submit", (event) => { event.preventDefault(); void h
registerForm.addEventListener("submit", (event) => { event.preventDefault(); void handleAuth("register", {username: registerUsernameInput.value.trim(), password: registerPasswordInput.value.trim()}); });
logoutButton.addEventListener("click", async () => {
closeChatDrawer();
await post("/logout", {}); chatDiv.style.display = "none"; authDiv.style.display = "grid";
await post("/logout", {}); chatDiv.classList.add("hidden"); authDiv.classList.remove("hidden");
activeChatId = null; currentUsername = ""; currentSummary = null; chatSessions = []; currentMessages = []; assistantPersonas = []; userPersonas = [];
messageRetryState = new Map();
activeUserPersonaId = null; publishedPersonaIds = new Set(); messagesDiv.innerHTML = ""; renderChatList(); updateChatActionState();
@@ -1904,8 +1973,9 @@ document.addEventListener("keydown", (event) => {
}
});
window.addEventListener("load", async () => {
applyTheme(localStorage.getItem("krishd-theme") || "fakegpt", false);
applyThemeMode(localStorage.getItem("krishd-theme-mode") || "light", false);
const appearance = readStoredAppearance();
applyTheme(appearance.theme, false);
applyThemeMode(appearance.mode, false);
applyWorkspaceMode(localStorage.getItem("krishd-workspace-mode") || "basic", false);
setNotice("Ready.");
updateChatActionState();
+4 -3
View File
@@ -6,10 +6,11 @@
<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"/>
<script src="app/theme-bootstrap.js"></script>
<link rel="stylesheet" href="style.css?v=20260809-2"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<body data-theme="fakegpt" data-theme-mode="light">
<div class="settings-shell card">
<header class="market-header market-header-simple">
<div>
@@ -298,6 +299,6 @@
</div>
</div>
</div>
<script src="settings.js" defer></script>
<script type="module" src="settings.js"></script>
</body>
</html>
+24 -55
View File
@@ -1,3 +1,12 @@
import {del, get, post, put} from "./app/api.js";
import {
applyTheme as applySharedTheme,
applyThemeMode as applySharedThemeMode,
initializeAppearance
} from "./app/themeController.js";
import {clearStatus, setStatus} from "./app/statusNotice.js";
import {populatePersonaForm, readPersonaForm} from "./app/personaForm.js";
const $ = (id) => document.getElementById(id);
const settingsNotice = $("settingsNotice");
const settingsThemeSelect = $("settingsThemeSelect");
@@ -56,14 +65,13 @@ const settingsPopupDescription = $("settingsPopupDescription");
const settingsPopupField = $("settingsPopupField");
const settingsPopupInputLabel = $("settingsPopupInputLabel");
const settingsPopupInput = $("settingsPopupInput");
const themes = {
"fakegpt": {name: "FakeGPT"},
"fraud": {name: "Fraud"},
"germini": {name: "Germini"},
"slopilot": {name: "Slopilot"},
"beta-ai": {name: "Beta AI"},
"confusity": {name: "Confusity"}
const settingsPersonaFields = {
name: settingsPersonaName,
pronouns: settingsPersonaPronouns,
appearance: settingsPersonaAppearance,
background: settingsPersonaBackground,
details: settingsPersonaDetails,
exampleDialogues: settingsPersonaExampleDialogues
};
let assistantPersonas = [];
@@ -73,38 +81,16 @@ let editingPersonaId = null;
let settingsPopupResolver = null;
let isAdminUser = false;
async function request(url, data, method = "POST") {
try {
const res = await fetch(url, {
method,
credentials: "same-origin",
headers: {"Content-Type": "application/json"},
body: data ? JSON.stringify(data) : undefined
});
return await res.json();
} catch {
return {error: "Network error - please try again."};
}
}
const get = (url) => request(url, null, "GET");
const post = (url, data) => request(url, data, "POST");
const put = (url, data) => request(url, data, "PUT");
const del = (url) => request(url, null, "DELETE");
function setNotice(message, state = "") {
settingsNotice.textContent = message;
settingsNotice.className = state ? `status ${state}` : "status";
setStatus(settingsNotice, message, state);
}
function setPersonaNotice(message = "", state = "") {
if (!message) {
settingsPersonaFormNotice.textContent = "";
settingsPersonaFormNotice.className = "status hidden";
clearStatus(settingsPersonaFormNotice);
return;
}
settingsPersonaFormNotice.textContent = message;
settingsPersonaFormNotice.className = state ? `status ${state}` : "status";
setStatus(settingsPersonaFormNotice, message, state);
}
function closeSettingsPopup(value = null) {
@@ -141,18 +127,11 @@ function promptSettingsPopup({
}
function applyTheme(themeKey, persist = true) {
const nextTheme = themes[themeKey] ? themeKey : "fakegpt";
document.body.dataset.theme = nextTheme;
document.title = "Settings";
settingsThemeSelect.value = nextTheme;
if (persist) localStorage.setItem("krishd-theme", nextTheme);
return applySharedTheme(themeKey, {persist, title: "Settings", themeSelect: settingsThemeSelect});
}
function applyThemeMode(modeKey, persist = true) {
const nextMode = modeKey === "dark" ? "dark" : "light";
document.body.dataset.themeMode = nextMode;
if (themeModeSelect) themeModeSelect.value = nextMode;
if (persist) localStorage.setItem("krishd-theme-mode", nextMode);
return applySharedThemeMode(modeKey, {persist, modeSelect: themeModeSelect});
}
function setSettingsView(view) {
@@ -247,12 +226,7 @@ function openPersonaModal(persona = null, personaType = "assistant") {
settingsPersonaFormTitle.textContent = persona
? `Edit ${settingsPersonaType.value === "assistant" ? "AI Character" : "persona"}`
: `Create ${personaType === "assistant" ? "AI Character" : "persona"}`;
settingsPersonaName.value = persona?.name || "";
settingsPersonaPronouns.value = persona?.pronouns || "";
settingsPersonaAppearance.value = persona?.appearance || "";
settingsPersonaBackground.value = persona?.background || "";
settingsPersonaDetails.value = persona?.details || "";
settingsPersonaExampleDialogues.value = persona?.example_dialogues || "";
populatePersonaForm(settingsPersonaFields, persona || {});
syncPersonaExamplesField();
setPersonaNotice("");
settingsPersonaModal.classList.remove("hidden");
@@ -359,12 +333,8 @@ async function loadPersonas() {
async function savePersona() {
const wasEditing = Boolean(editingPersonaId);
const payload = {
...readPersonaForm(settingsPersonaFields),
personaType: settingsPersonaType.value,
name: settingsPersonaName.value.trim(),
pronouns: settingsPersonaPronouns.value.trim(),
appearance: settingsPersonaAppearance.value.trim(),
background: settingsPersonaBackground.value.trim(),
details: settingsPersonaDetails.value.trim(),
exampleDialogues: settingsPersonaType.value === "assistant" ? settingsPersonaExampleDialogues.value.trim() : ""
};
@@ -602,8 +572,7 @@ settingsPopupInput.addEventListener("keydown", (event) => {
});
window.addEventListener("load", async () => {
applyTheme(localStorage.getItem("krishd-theme") || "fakegpt", false);
applyThemeMode(localStorage.getItem("krishd-theme-mode") || "light", false);
initializeAppearance({title: "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";
+563 -6516
View File
File diff suppressed because it is too large Load Diff