Overhaul legal page and global styles for consistency and responsiveness.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
@@ -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 & 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
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user