Overhaul legal page and global styles for consistency and responsiveness.
This commit is contained in:
@@ -69,13 +69,19 @@ Legacy `chats` migration is handled during startup. New code only reads the sess
|
|||||||
|
|
||||||
## Frontend Structure
|
## Frontend Structure
|
||||||
|
|
||||||
The main browser entrypoint is still `public/script.js`, but shared API/bootstrap concerns are now split into `public/app/` modules:
|
The browser UI remains framework-free and uses native ES modules. Shared behavior lives under `public/app/`:
|
||||||
|
|
||||||
- `api.js`: JSON request helpers
|
- `api.js`: JSON and streaming request helpers
|
||||||
- `constants.js`: theme and onboarding constants
|
- `constants.js`: theme and onboarding metadata
|
||||||
- `dom.js`: cached DOM references
|
- `themeController.js`: validated theme/mode persistence and application
|
||||||
|
- `theme-bootstrap.js`: applies stored appearance before page initialization
|
||||||
|
- `personaForm.js`: shared persona form serialization
|
||||||
|
- `statusNotice.js`: shared status and error presentation
|
||||||
|
- `dom.js`: cached chat-page DOM references
|
||||||
|
|
||||||
This keeps the next round of UI extraction lower-risk without changing the app's behavior surface.
|
`public/style.css` is a self-contained design system rebuilt around semantic tokens, shared components, and deliberate responsive breakpoints. It contains the six visual identities and their dark variants without legacy imports or precedence layers. `public/legal.css` contains only the legal-page layout.
|
||||||
|
|
||||||
|
Pure chat message, retry, memory, summary, and persona-state formatting lives in `src/services/chatMessageHelpers.js`; `chatService.js` remains responsible for orchestration and persistence.
|
||||||
|
|
||||||
## Legal
|
## Legal
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,20 @@ function toggleChatDrawer() {
|
|||||||
syncChatDrawerState(!isChatDrawerOpen());
|
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) {
|
function handleDocumentClick(event) {
|
||||||
if (!isChatDrawerOpen()) return;
|
if (!isChatDrawerOpen()) return;
|
||||||
const target = event.target;
|
const target = event.target;
|
||||||
@@ -103,6 +117,7 @@ if (chatDrawerCloseBtn) {
|
|||||||
|
|
||||||
document.addEventListener("click", handleDocumentClick);
|
document.addEventListener("click", handleDocumentClick);
|
||||||
document.addEventListener("keydown", handleDocumentKeydown);
|
document.addEventListener("keydown", handleDocumentKeydown);
|
||||||
|
chatDrawer?.addEventListener("toggle", handleDisclosureToggle, true);
|
||||||
|
|
||||||
export {
|
export {
|
||||||
closeChatDrawer,
|
closeChatDrawer,
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ const messagesDiv = $("messages");
|
|||||||
const modelSelect = $("model");
|
const modelSelect = $("model");
|
||||||
const modelPanel = $("modelPanel");
|
const modelPanel = $("modelPanel");
|
||||||
const modelBadgeName = $("modelBadgeName");
|
const modelBadgeName = $("modelBadgeName");
|
||||||
|
const headerModelName = $("headerModelName");
|
||||||
|
const headerPersonaName = $("headerPersonaName");
|
||||||
const modelCount = $("modelCount");
|
const modelCount = $("modelCount");
|
||||||
const modelHelpTitle = $("modelHelpTitle");
|
const modelHelpTitle = $("modelHelpTitle");
|
||||||
const modelHelpBadge = $("modelHelpBadge");
|
const modelHelpBadge = $("modelHelpBadge");
|
||||||
@@ -41,6 +43,7 @@ const exportChatBtn = $("exportChat");
|
|||||||
const pinChatBtn = $("pinChat");
|
const pinChatBtn = $("pinChat");
|
||||||
const moveChatFolderBtn = $("moveChatFolder");
|
const moveChatFolderBtn = $("moveChatFolder");
|
||||||
const manageChatMemoryBtn = $("manageChatMemory");
|
const manageChatMemoryBtn = $("manageChatMemory");
|
||||||
|
const managePersonaStateBtn = $("managePersonaState");
|
||||||
const archiveChatBtn = $("archiveChat");
|
const archiveChatBtn = $("archiveChat");
|
||||||
const backupWorkspaceBtn = $("backupWorkspace");
|
const backupWorkspaceBtn = $("backupWorkspace");
|
||||||
const importWorkspaceBtn = $("importWorkspace");
|
const importWorkspaceBtn = $("importWorkspace");
|
||||||
@@ -139,6 +142,7 @@ export {
|
|||||||
pinChatBtn,
|
pinChatBtn,
|
||||||
moveChatFolderBtn,
|
moveChatFolderBtn,
|
||||||
manageChatMemoryBtn,
|
manageChatMemoryBtn,
|
||||||
|
managePersonaStateBtn,
|
||||||
archiveChatBtn,
|
archiveChatBtn,
|
||||||
backupWorkspaceBtn,
|
backupWorkspaceBtn,
|
||||||
importWorkspaceBtn,
|
importWorkspaceBtn,
|
||||||
@@ -150,6 +154,8 @@ export {
|
|||||||
logoutButton,
|
logoutButton,
|
||||||
messagesDiv,
|
messagesDiv,
|
||||||
modelBadgeName,
|
modelBadgeName,
|
||||||
|
headerModelName,
|
||||||
|
headerPersonaName,
|
||||||
modelCount,
|
modelCount,
|
||||||
modelHelpBadge,
|
modelHelpBadge,
|
||||||
modelHelpSummary,
|
modelHelpSummary,
|
||||||
|
|||||||
+21
-16
@@ -6,10 +6,11 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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 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">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body data-theme="fakegpt" data-theme-mode="light">
|
||||||
<div class="app-shell">
|
<div class="app-shell">
|
||||||
<section id="auth" class="card auth-card auth-card-compact">
|
<section id="auth" class="card auth-card auth-card-compact">
|
||||||
<div class="auth-hero auth-hero-compact">
|
<div class="auth-hero auth-hero-compact">
|
||||||
@@ -72,7 +73,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
<div class="chat-layout chat-layout-focus">
|
||||||
<aside id="chatSidebar" class="chat-sidebar" aria-label="Workspace navigation">
|
<aside id="chatSidebar" class="chat-sidebar" aria-label="Workspace navigation">
|
||||||
<div class="sidebar-top">
|
<div class="sidebar-top">
|
||||||
@@ -111,24 +112,27 @@
|
|||||||
<div class="chat-title">
|
<div class="chat-title">
|
||||||
<h2 id="activeChatTitle">New chat</h2>
|
<h2 id="activeChatTitle">New chat</h2>
|
||||||
</div>
|
</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>
|
||||||
<div class="chat-header-actions">
|
<div class="chat-header-actions">
|
||||||
<div class="chat-actions-menu">
|
<div class="chat-actions-menu">
|
||||||
<button id="chatDrawerButton" class="secondary-action overflow-trigger" type="button" aria-expanded="false" aria-controls="chatDrawer">
|
<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>
|
</button>
|
||||||
<div id="chatDrawer" class="chat-drawer hidden" role="dialog" aria-modal="true" aria-labelledby="chatDrawerTitle">
|
<div id="chatDrawer" class="chat-drawer hidden" role="dialog" aria-modal="true" aria-labelledby="chatDrawerTitle">
|
||||||
<div class="chat-drawer-panel">
|
<div class="chat-drawer-panel">
|
||||||
<div class="chat-drawer-header">
|
<div class="chat-drawer-header">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Conversation</p>
|
<p class="eyebrow">Conversation</p>
|
||||||
<h2 id="chatDrawerTitle">Chat controls</h2>
|
<h2 id="chatDrawerTitle">Settings</h2>
|
||||||
</div>
|
</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>
|
||||||
<div class="chat-drawer-body">
|
<div class="chat-drawer-body">
|
||||||
<div class="chat-drawer-summary">
|
<div class="chat-drawer-summary">
|
||||||
<p class="dropdown-section-label">Current mode</p>
|
|
||||||
<div class="chat-drawer-pills">
|
<div class="chat-drawer-pills">
|
||||||
<span id="chatCharacterPill" class="context-pill hidden">Character: none</span>
|
<span id="chatCharacterPill" class="context-pill hidden">Character: none</span>
|
||||||
<span id="chatUserPersonaPill" class="context-pill hidden">User persona: 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">
|
<details class="chat-drawer-section chat-drawer-disclosure chat-drawer-section--model">
|
||||||
<summary class="chat-drawer-row">
|
<summary class="chat-drawer-row">
|
||||||
<span class="chat-drawer-row-label">
|
<span class="chat-drawer-row-label">
|
||||||
<span class="eyebrow">Model</span>
|
<span class="eyebrow">AI model</span>
|
||||||
<strong>Engine</strong>
|
<strong>Model</strong>
|
||||||
</span>
|
</span>
|
||||||
<span class="chat-drawer-row-meta">
|
<span class="chat-drawer-row-meta">
|
||||||
<strong id="modelBadgeName">Loading...</strong>
|
<strong id="modelBadgeName">Loading...</strong>
|
||||||
@@ -170,8 +174,8 @@
|
|||||||
<details class="chat-drawer-section chat-drawer-disclosure chat-drawer-section--persona">
|
<details class="chat-drawer-section chat-drawer-disclosure chat-drawer-section--persona">
|
||||||
<summary class="chat-drawer-row">
|
<summary class="chat-drawer-row">
|
||||||
<span class="chat-drawer-row-label">
|
<span class="chat-drawer-row-label">
|
||||||
<span class="eyebrow">Persona</span>
|
<span class="eyebrow">Your identity</span>
|
||||||
<strong>Identity</strong>
|
<strong>Persona</strong>
|
||||||
</span>
|
</span>
|
||||||
<span class="chat-drawer-row-meta">
|
<span class="chat-drawer-row-meta">
|
||||||
<strong id="chatDrawerPersonaValue">None</strong>
|
<strong id="chatDrawerPersonaValue">None</strong>
|
||||||
@@ -195,7 +199,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
<div class="chat-drawer-section">
|
<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">
|
<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="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>
|
<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">
|
<details class="chat-drawer-section chat-drawer-more">
|
||||||
<summary class="chat-drawer-row chat-drawer-row--compact">
|
<summary class="chat-drawer-row chat-drawer-row--compact">
|
||||||
<span class="chat-drawer-row-label">
|
<span class="chat-drawer-row-label">
|
||||||
<strong>More actions</strong>
|
<strong>Tools & workspace</strong>
|
||||||
</span>
|
</span>
|
||||||
<span class="chat-drawer-row-hint">Expand</span>
|
<span class="chat-drawer-row-hint">Show</span>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="chat-drawer-action-list chat-drawer-disclosure-panel">
|
<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="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="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="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>
|
<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"/>
|
<input id="importWorkspaceFile" class="hidden" type="file" accept="application/json,.json"/>
|
||||||
@@ -222,9 +227,9 @@
|
|||||||
<details class="chat-drawer-section chat-drawer-danger">
|
<details class="chat-drawer-section chat-drawer-danger">
|
||||||
<summary class="chat-drawer-row chat-drawer-row--compact">
|
<summary class="chat-drawer-row chat-drawer-row--compact">
|
||||||
<span class="chat-drawer-row-label">
|
<span class="chat-drawer-row-label">
|
||||||
<strong>Danger zone</strong>
|
<strong>Clear conversation</strong>
|
||||||
</span>
|
</span>
|
||||||
<span class="chat-drawer-row-hint">Expand</span>
|
<span class="chat-drawer-row-hint">Show</span>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="chat-drawer-action-list chat-drawer-disclosure-panel">
|
<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>
|
<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 {
|
.legal-page {
|
||||||
display: block;
|
min-height: calc(100dvh - 36px);
|
||||||
height: auto;
|
display: grid;
|
||||||
background-attachment: fixed;
|
place-items: center;
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: cover;
|
|
||||||
min-height: 100%;
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
padding: 0;
|
|
||||||
}
|
}
|
||||||
|
.legal-container,
|
||||||
.legal-content {
|
.legal-content {
|
||||||
max-width: 900px;
|
width: min(860px, 100%);
|
||||||
margin: 0 auto;
|
padding: clamp(24px, 5vw, 52px);
|
||||||
padding: 2rem 1.5rem;
|
background: var(--surface-raised);
|
||||||
color: var(--text-primary);
|
border: 1px solid var(--border);
|
||||||
font-family: inherit;
|
border-radius: var(--radius-xl);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
|
.legal-container h1, .legal-content h1 { margin-bottom: 20px; }
|
||||||
.legal-content h1,
|
.legal-container h2, .legal-content h2 { margin: 28px 0 10px; font-size: 1.25rem; }
|
||||||
.legal-content h2 {
|
.legal-container p, .legal-container li, .legal-content p, .legal-content li { color: var(--text-soft); line-height: 1.75; }
|
||||||
color: var(--text-primary);
|
.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; }
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-3
@@ -6,10 +6,11 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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 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">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body data-theme="fakegpt" data-theme-mode="light">
|
||||||
<div class="market-shell card market-shell-simple">
|
<div class="market-shell card market-shell-simple">
|
||||||
<header class="market-header market-header-simple">
|
<header class="market-header market-header-simple">
|
||||||
<div>
|
<div>
|
||||||
@@ -237,6 +238,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="market.js" defer></script>
|
<script type="module" src="market.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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 $ = (id) => document.getElementById(id);
|
||||||
const marketPersonaList = $("marketPersonaList");
|
const marketPersonaList = $("marketPersonaList");
|
||||||
const marketUserPersonaList = $("marketUserPersonaList");
|
const marketUserPersonaList = $("marketUserPersonaList");
|
||||||
@@ -55,14 +60,13 @@ const marketPopupField = $("marketPopupField");
|
|||||||
const marketPopupInputLabel = $("marketPopupInputLabel");
|
const marketPopupInputLabel = $("marketPopupInputLabel");
|
||||||
const marketPopupInput = $("marketPopupInput");
|
const marketPopupInput = $("marketPopupInput");
|
||||||
const themeNameTargets = document.querySelectorAll("[data-theme-name]");
|
const themeNameTargets = document.querySelectorAll("[data-theme-name]");
|
||||||
|
const marketPersonaFields = {
|
||||||
const themes = {
|
name: marketPersonaName,
|
||||||
"fakegpt": {name: "FakeGPT"},
|
pronouns: marketPersonaPronouns,
|
||||||
"fraud": {name: "Fraud"},
|
appearance: marketPersonaAppearance,
|
||||||
"germini": {name: "Germini"},
|
background: marketPersonaBackground,
|
||||||
"slopilot": {name: "Slopilot"},
|
details: marketPersonaDetails,
|
||||||
"beta-ai": {name: "Beta AI"},
|
exampleDialogues: marketPersonaExampleDialogues
|
||||||
"confusity": {name: "Confusity"}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const validMarketSorts = new Set(["best", "newest", "most_favorited", "most_popular", "top_rated", "alphabetical"]);
|
const validMarketSorts = new Set(["best", "newest", "most_favorited", "most_popular", "top_rated", "alphabetical"]);
|
||||||
@@ -78,40 +82,8 @@ let marketActivityDepth = 0;
|
|||||||
let marketPreviewStep = 1;
|
let marketPreviewStep = 1;
|
||||||
let marketPopupResolver = null;
|
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 = "") {
|
function setMarketStatus(message, state = "") {
|
||||||
marketStatus.textContent = message;
|
setStatus(marketStatus, message, state, "status persona-status");
|
||||||
marketStatus.className = state ? `status persona-status ${state}` : "status persona-status";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeMarketPopup(value = null) {
|
function closeMarketPopup(value = null) {
|
||||||
@@ -150,12 +122,10 @@ function promptMarketPopup({
|
|||||||
|
|
||||||
function setPersonaFormNotice(message = "", state = "") {
|
function setPersonaFormNotice(message = "", state = "") {
|
||||||
if (!message) {
|
if (!message) {
|
||||||
marketPersonaFormNotice.textContent = "";
|
clearStatus(marketPersonaFormNotice);
|
||||||
marketPersonaFormNotice.className = "status hidden";
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
marketPersonaFormNotice.textContent = message;
|
setStatus(marketPersonaFormNotice, message, state);
|
||||||
marketPersonaFormNotice.className = state ? `status ${state}` : "status";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setMarketActivity(active, {
|
function setMarketActivity(active, {
|
||||||
@@ -351,12 +321,7 @@ function openMarketPreview(persona) {
|
|||||||
function openPersonaForm(persona = null) {
|
function openPersonaForm(persona = null) {
|
||||||
editingPersonaId = persona ? persona.id : null;
|
editingPersonaId = persona ? persona.id : null;
|
||||||
marketPersonaFormTitle.textContent = persona ? "Edit AI Character" : "Create AI Character";
|
marketPersonaFormTitle.textContent = persona ? "Edit AI Character" : "Create AI Character";
|
||||||
marketPersonaName.value = persona?.name || "";
|
populatePersonaForm(marketPersonaFields, persona || {});
|
||||||
marketPersonaPronouns.value = persona?.pronouns || "";
|
|
||||||
marketPersonaAppearance.value = persona?.appearance || "";
|
|
||||||
marketPersonaBackground.value = persona?.background || "";
|
|
||||||
marketPersonaDetails.value = persona?.details || "";
|
|
||||||
marketPersonaExampleDialogues.value = persona?.example_dialogues || "";
|
|
||||||
setPersonaFormNotice("");
|
setPersonaFormNotice("");
|
||||||
marketPersonaModal.classList.remove("hidden");
|
marketPersonaModal.classList.remove("hidden");
|
||||||
marketPersonaName.focus();
|
marketPersonaName.focus();
|
||||||
@@ -600,13 +565,8 @@ async function startMarketPersonaChat(marketId, userPersonaSelection, scenarioPr
|
|||||||
|
|
||||||
async function savePersona() {
|
async function savePersona() {
|
||||||
const payload = {
|
const payload = {
|
||||||
|
...readPersonaForm(marketPersonaFields),
|
||||||
personaType: "assistant",
|
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) {
|
if (!payload.name) {
|
||||||
@@ -713,8 +673,7 @@ marketPersonaForm.addEventListener("submit", (event) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener("load", async () => {
|
window.addEventListener("load", async () => {
|
||||||
applyTheme(localStorage.getItem("krishd-theme") || "fakegpt");
|
initializeAppearance({title: (theme) => `${theme.name} Market`, nameTargets: themeNameTargets});
|
||||||
applyThemeMode(localStorage.getItem("krishd-theme-mode") || "light");
|
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
setMarketSort("assistant", params.get("assistantSort") || "best", {reload: false});
|
setMarketSort("assistant", params.get("assistantSort") || "best", {reload: false});
|
||||||
setMarketSort("user", params.get("userSort") || "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 {del, get, post, put, stream} from "./app/api.js";
|
||||||
import {closeChatDrawer} from "./app/chatDrawer.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 {
|
import {
|
||||||
activeChatTitle,
|
activeChatTitle,
|
||||||
activeUserPersonaStatus,
|
activeUserPersonaStatus,
|
||||||
@@ -28,7 +29,10 @@ import {
|
|||||||
exportChatBtn,
|
exportChatBtn,
|
||||||
importWorkspaceBtn,
|
importWorkspaceBtn,
|
||||||
manageChatMemoryBtn,
|
manageChatMemoryBtn,
|
||||||
|
managePersonaStateBtn,
|
||||||
importWorkspaceFileInput,
|
importWorkspaceFileInput,
|
||||||
|
headerModelName,
|
||||||
|
headerPersonaName,
|
||||||
moveChatFolderBtn,
|
moveChatFolderBtn,
|
||||||
loginForm,
|
loginForm,
|
||||||
loginPasswordInput,
|
loginPasswordInput,
|
||||||
@@ -559,6 +563,7 @@ function addMessage(contentOrMessage, isUser = false, isLoading = false, options
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
msgDiv.setAttribute("aria-label", "Assistant is responding");
|
msgDiv.setAttribute("aria-label", "Assistant is responding");
|
||||||
msgDiv.innerHTML = `
|
msgDiv.innerHTML = `
|
||||||
|
<div class="msg-header">Assistant</div>
|
||||||
<div class="loading-shell" aria-live="polite" aria-label="Assistant is responding">
|
<div class="loading-shell" aria-live="polite" aria-label="Assistant is responding">
|
||||||
<div class="loading-bars">
|
<div class="loading-bars">
|
||||||
<span class="loading-bar"></span>
|
<span class="loading-bar"></span>
|
||||||
@@ -569,7 +574,8 @@ function addMessage(contentOrMessage, isUser = false, isLoading = false, options
|
|||||||
<div class="loading-copy">
|
<div class="loading-copy">
|
||||||
<strong>Thinking</strong>
|
<strong>Thinking</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>
|
||||||
|
<div class="msg-content streaming-content"></div>`;
|
||||||
} else {
|
} else {
|
||||||
const message = typeof contentOrMessage === "object" && contentOrMessage
|
const message = typeof contentOrMessage === "object" && contentOrMessage
|
||||||
? contentOrMessage
|
? contentOrMessage
|
||||||
@@ -728,7 +734,13 @@ function renderMessageEmptyState() {
|
|||||||
createStarterAction("Change setup", "secondary-action", () => openRoleplayStarter())
|
createStarterAction("Change setup", "secondary-action", () => openRoleplayStarter())
|
||||||
);
|
);
|
||||||
} else {
|
} 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);
|
empty.append(title, description, actions);
|
||||||
@@ -849,6 +861,7 @@ function updateModelHelp() {
|
|||||||
if (!selectedOption || selectedOption.disabled) {
|
if (!selectedOption || selectedOption.disabled) {
|
||||||
if (modelCount) modelCount.textContent = "No models";
|
if (modelCount) modelCount.textContent = "No models";
|
||||||
if (modelBadgeName) modelBadgeName.textContent = "Unavailable";
|
if (modelBadgeName) modelBadgeName.textContent = "Unavailable";
|
||||||
|
if (headerModelName) headerModelName.textContent = "Unavailable";
|
||||||
modelHelpBadge.dataset.badge = "waiting";
|
modelHelpBadge.dataset.badge = "waiting";
|
||||||
modelHelpTitle.textContent = "Choose a model";
|
modelHelpTitle.textContent = "Choose a model";
|
||||||
modelHelpBadge.textContent = "Waiting";
|
modelHelpBadge.textContent = "Waiting";
|
||||||
@@ -863,6 +876,7 @@ function updateModelHelp() {
|
|||||||
const modelName = selectedOption.textContent || modelId;
|
const modelName = selectedOption.textContent || modelId;
|
||||||
const profile = getModelProfile(modelId, modelName);
|
const profile = getModelProfile(modelId, modelName);
|
||||||
if (modelBadgeName) modelBadgeName.textContent = modelName;
|
if (modelBadgeName) modelBadgeName.textContent = modelName;
|
||||||
|
if (headerModelName) headerModelName.textContent = modelName;
|
||||||
modelHelpTitle.textContent = modelName;
|
modelHelpTitle.textContent = modelName;
|
||||||
modelHelpBadge.textContent = profile.badge;
|
modelHelpBadge.textContent = profile.badge;
|
||||||
modelHelpBadge.dataset.badge = profile.badge.toLowerCase();
|
modelHelpBadge.dataset.badge = profile.badge.toLowerCase();
|
||||||
@@ -870,21 +884,18 @@ function updateModelHelp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applyTheme(themeKey, persist = true) {
|
function applyTheme(themeKey, persist = true) {
|
||||||
const nextTheme = themes[themeKey] ? themeKey : "fakegpt";
|
applySharedTheme(themeKey, {
|
||||||
const theme = themes[nextTheme];
|
persist,
|
||||||
document.body.dataset.theme = nextTheme;
|
title: (theme) => theme.name,
|
||||||
document.title = theme.name;
|
nameTargets: themeNameTargets,
|
||||||
themeNameTargets.forEach((target) => { target.textContent = theme.name; });
|
logoTargets: themeLogoTargets
|
||||||
themeLogoTargets.forEach((target) => { target.textContent = theme.short; });
|
});
|
||||||
if (persist) localStorage.setItem("krishd-theme", nextTheme);
|
|
||||||
updateWorkspaceCopy();
|
updateWorkspaceCopy();
|
||||||
renderModelSection();
|
renderModelSection();
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyThemeMode(modeKey, persist = true) {
|
function applyThemeMode(modeKey, persist = true) {
|
||||||
const nextMode = modeKey === "dark" ? "dark" : "light";
|
applySharedThemeMode(modeKey, {persist});
|
||||||
document.body.dataset.themeMode = nextMode;
|
|
||||||
if (persist) localStorage.setItem("krishd-theme-mode", nextMode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateChatActionState() {
|
function updateChatActionState() {
|
||||||
@@ -897,6 +908,7 @@ function updateChatActionState() {
|
|||||||
}
|
}
|
||||||
if (moveChatFolderBtn) moveChatFolderBtn.disabled = !hasChat;
|
if (moveChatFolderBtn) moveChatFolderBtn.disabled = !hasChat;
|
||||||
if (manageChatMemoryBtn) manageChatMemoryBtn.disabled = !hasChat;
|
if (manageChatMemoryBtn) manageChatMemoryBtn.disabled = !hasChat;
|
||||||
|
if (managePersonaStateBtn) managePersonaStateBtn.disabled = !hasChat || !activeChat?.assistant_persona_id;
|
||||||
if (archiveChatBtn) {
|
if (archiveChatBtn) {
|
||||||
archiveChatBtn.disabled = !hasChat;
|
archiveChatBtn.disabled = !hasChat;
|
||||||
if (hasChat) archiveChatBtn.textContent = activeChat.archived_at ? "Unarchive" : "Archive";
|
if (hasChat) archiveChatBtn.textContent = activeChat.archived_at ? "Unarchive" : "Archive";
|
||||||
@@ -912,6 +924,7 @@ function renderPersonaSection() {
|
|||||||
if (chatDrawerPersonaValue) {
|
if (chatDrawerPersonaValue) {
|
||||||
const active = userPersonas.find((persona) => persona.id === activeUserPersonaId);
|
const active = userPersonas.find((persona) => persona.id === activeUserPersonaId);
|
||||||
chatDrawerPersonaValue.textContent = active ? active.name : "None";
|
chatDrawerPersonaValue.textContent = active ? active.name : "None";
|
||||||
|
if (headerPersonaName) headerPersonaName.textContent = active ? active.name : "None";
|
||||||
}
|
}
|
||||||
if (clearUserPersonaBtn) clearUserPersonaBtn.disabled = !activeUserPersonaId;
|
if (clearUserPersonaBtn) clearUserPersonaBtn.disabled = !activeUserPersonaId;
|
||||||
}
|
}
|
||||||
@@ -1280,7 +1293,11 @@ function setLoadingState(loading, overlayOptions = null) {
|
|||||||
sendBtn.classList.toggle("loading", loading);
|
sendBtn.classList.toggle("loading", loading);
|
||||||
if (loading) {
|
if (loading) {
|
||||||
msgInput.placeholder = "Processing response...";
|
msgInput.placeholder = "Processing response...";
|
||||||
setChatActivity(true, overlayOptions || undefined);
|
if (overlayOptions === false) {
|
||||||
|
setChatActivity(false);
|
||||||
|
} else {
|
||||||
|
setChatActivity(true, overlayOptions || undefined);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setChatActivity(false);
|
setChatActivity(false);
|
||||||
updateComposerPlaceholder();
|
updateComposerPlaceholder();
|
||||||
@@ -1590,6 +1607,54 @@ async function manageChatMemory() {
|
|||||||
return setNotice("Memory fact saved.", "success");
|
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) {
|
async function deleteChat(id) {
|
||||||
setChatLoading(true, "Deleting chat...");
|
setChatLoading(true, "Deleting chat...");
|
||||||
const chat = getChatById(id);
|
const chat = getChatById(id);
|
||||||
@@ -1647,7 +1712,7 @@ async function checkSession() {
|
|||||||
const res = await get("/session");
|
const res = await get("/session");
|
||||||
if (registerInviteField) registerInviteField.classList.toggle("hidden", !res.inviteOnlyRegistration);
|
if (registerInviteField) registerInviteField.classList.toggle("hidden", !res.inviteOnlyRegistration);
|
||||||
if (!res.user) return false;
|
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()]);
|
await Promise.all([displayModels(), loadSummary(), loadChatSessions(), loadPersonas()]);
|
||||||
maybeShowOnboarding();
|
maybeShowOnboarding();
|
||||||
msgInput.focus(); return true;
|
msgInput.focus(); return true;
|
||||||
@@ -1664,7 +1729,7 @@ async function handleAuth(endpoint, credentials) {
|
|||||||
submitBtn.disabled = false;
|
submitBtn.disabled = false;
|
||||||
if (res.error) return setAuthMessage(res.error, "error");
|
if (res.error) return setAuthMessage(res.error, "error");
|
||||||
if (endpoint === "login") {
|
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()]);
|
await Promise.all([displayModels(), loadSummary(), loadChatSessions(), loadPersonas()]);
|
||||||
maybeShowOnboarding();
|
maybeShowOnboarding();
|
||||||
setNotice("Ready.", "success"); msgInput.focus();
|
setNotice("Ready.", "success"); msgInput.focus();
|
||||||
@@ -1687,18 +1752,19 @@ async function sendMessage() {
|
|||||||
currentMessages.push({role: "user", content: message}); addMessage(message, true); updateChatActionState();
|
currentMessages.push({role: "user", content: message}); addMessage(message, true); updateChatActionState();
|
||||||
msgInput.value = "";
|
msgInput.value = "";
|
||||||
resizeComposerInput();
|
resizeComposerInput();
|
||||||
setLoadingState(true, {
|
setLoadingState(true, false); setNotice("Generating reply...");
|
||||||
eyebrow: "Assistant replying",
|
const loadingMsg = addMessage({role: "bot", content: ""}, false, true, {isNewest: true});
|
||||||
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});
|
|
||||||
const loadingBody = loadingMsg.querySelector(".msg-content");
|
const loadingBody = loadingMsg.querySelector(".msg-content");
|
||||||
|
const loadingShell = loadingMsg.querySelector(".loading-shell");
|
||||||
try {
|
try {
|
||||||
let streamError = null;
|
let streamError = null;
|
||||||
await stream("/chat/stream", {message, model: modelSelect.value, chatId: activeChatId}, {
|
await stream("/chat/stream", {message, model: modelSelect.value, chatId: activeChatId}, {
|
||||||
onChunk: ({fullReply}) => {
|
onChunk: ({fullReply}) => {
|
||||||
if (loadingBody) setMessageContent(loadingBody, fullReply);
|
if (loadingBody) {
|
||||||
|
loadingShell?.classList.add("hidden");
|
||||||
|
loadingBody.classList.add("has-content");
|
||||||
|
setMessageContent(loadingBody, fullReply);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onDone: () => {},
|
onDone: () => {},
|
||||||
onError: ({error}) => {
|
onError: ({error}) => {
|
||||||
@@ -1752,6 +1818,9 @@ const chatActions = {
|
|||||||
memory: async () => {
|
memory: async () => {
|
||||||
await manageChatMemory();
|
await manageChatMemory();
|
||||||
},
|
},
|
||||||
|
personaState: async () => {
|
||||||
|
await managePersonaState();
|
||||||
|
},
|
||||||
pin: async () => {
|
pin: async () => {
|
||||||
const chat = getChatById(activeChatId);
|
const chat = getChatById(activeChatId);
|
||||||
if (chat) await updateChatOrganization(chat.id, {isPinned: !chat.is_pinned});
|
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()}); });
|
registerForm.addEventListener("submit", (event) => { event.preventDefault(); void handleAuth("register", {username: registerUsernameInput.value.trim(), password: registerPasswordInput.value.trim()}); });
|
||||||
logoutButton.addEventListener("click", async () => {
|
logoutButton.addEventListener("click", async () => {
|
||||||
closeChatDrawer();
|
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 = [];
|
activeChatId = null; currentUsername = ""; currentSummary = null; chatSessions = []; currentMessages = []; assistantPersonas = []; userPersonas = [];
|
||||||
messageRetryState = new Map();
|
messageRetryState = new Map();
|
||||||
activeUserPersonaId = null; publishedPersonaIds = new Set(); messagesDiv.innerHTML = ""; renderChatList(); updateChatActionState();
|
activeUserPersonaId = null; publishedPersonaIds = new Set(); messagesDiv.innerHTML = ""; renderChatList(); updateChatActionState();
|
||||||
@@ -1904,8 +1973,9 @@ document.addEventListener("keydown", (event) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
window.addEventListener("load", async () => {
|
window.addEventListener("load", async () => {
|
||||||
applyTheme(localStorage.getItem("krishd-theme") || "fakegpt", false);
|
const appearance = readStoredAppearance();
|
||||||
applyThemeMode(localStorage.getItem("krishd-theme-mode") || "light", false);
|
applyTheme(appearance.theme, false);
|
||||||
|
applyThemeMode(appearance.mode, false);
|
||||||
applyWorkspaceMode(localStorage.getItem("krishd-workspace-mode") || "basic", false);
|
applyWorkspaceMode(localStorage.getItem("krishd-workspace-mode") || "basic", false);
|
||||||
setNotice("Ready.");
|
setNotice("Ready.");
|
||||||
updateChatActionState();
|
updateChatActionState();
|
||||||
|
|||||||
@@ -6,10 +6,11 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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 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">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body data-theme="fakegpt" data-theme-mode="light">
|
||||||
<div class="settings-shell card">
|
<div class="settings-shell card">
|
||||||
<header class="market-header market-header-simple">
|
<header class="market-header market-header-simple">
|
||||||
<div>
|
<div>
|
||||||
@@ -298,6 +299,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="settings.js" defer></script>
|
<script type="module" src="settings.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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 $ = (id) => document.getElementById(id);
|
||||||
const settingsNotice = $("settingsNotice");
|
const settingsNotice = $("settingsNotice");
|
||||||
const settingsThemeSelect = $("settingsThemeSelect");
|
const settingsThemeSelect = $("settingsThemeSelect");
|
||||||
@@ -56,14 +65,13 @@ const settingsPopupDescription = $("settingsPopupDescription");
|
|||||||
const settingsPopupField = $("settingsPopupField");
|
const settingsPopupField = $("settingsPopupField");
|
||||||
const settingsPopupInputLabel = $("settingsPopupInputLabel");
|
const settingsPopupInputLabel = $("settingsPopupInputLabel");
|
||||||
const settingsPopupInput = $("settingsPopupInput");
|
const settingsPopupInput = $("settingsPopupInput");
|
||||||
|
const settingsPersonaFields = {
|
||||||
const themes = {
|
name: settingsPersonaName,
|
||||||
"fakegpt": {name: "FakeGPT"},
|
pronouns: settingsPersonaPronouns,
|
||||||
"fraud": {name: "Fraud"},
|
appearance: settingsPersonaAppearance,
|
||||||
"germini": {name: "Germini"},
|
background: settingsPersonaBackground,
|
||||||
"slopilot": {name: "Slopilot"},
|
details: settingsPersonaDetails,
|
||||||
"beta-ai": {name: "Beta AI"},
|
exampleDialogues: settingsPersonaExampleDialogues
|
||||||
"confusity": {name: "Confusity"}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let assistantPersonas = [];
|
let assistantPersonas = [];
|
||||||
@@ -73,38 +81,16 @@ let editingPersonaId = null;
|
|||||||
let settingsPopupResolver = null;
|
let settingsPopupResolver = null;
|
||||||
let isAdminUser = false;
|
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 = "") {
|
function setNotice(message, state = "") {
|
||||||
settingsNotice.textContent = message;
|
setStatus(settingsNotice, message, state);
|
||||||
settingsNotice.className = state ? `status ${state}` : "status";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setPersonaNotice(message = "", state = "") {
|
function setPersonaNotice(message = "", state = "") {
|
||||||
if (!message) {
|
if (!message) {
|
||||||
settingsPersonaFormNotice.textContent = "";
|
clearStatus(settingsPersonaFormNotice);
|
||||||
settingsPersonaFormNotice.className = "status hidden";
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
settingsPersonaFormNotice.textContent = message;
|
setStatus(settingsPersonaFormNotice, message, state);
|
||||||
settingsPersonaFormNotice.className = state ? `status ${state}` : "status";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeSettingsPopup(value = null) {
|
function closeSettingsPopup(value = null) {
|
||||||
@@ -141,18 +127,11 @@ function promptSettingsPopup({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applyTheme(themeKey, persist = true) {
|
function applyTheme(themeKey, persist = true) {
|
||||||
const nextTheme = themes[themeKey] ? themeKey : "fakegpt";
|
return applySharedTheme(themeKey, {persist, title: "Settings", themeSelect: settingsThemeSelect});
|
||||||
document.body.dataset.theme = nextTheme;
|
|
||||||
document.title = "Settings";
|
|
||||||
settingsThemeSelect.value = nextTheme;
|
|
||||||
if (persist) localStorage.setItem("krishd-theme", nextTheme);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyThemeMode(modeKey, persist = true) {
|
function applyThemeMode(modeKey, persist = true) {
|
||||||
const nextMode = modeKey === "dark" ? "dark" : "light";
|
return applySharedThemeMode(modeKey, {persist, modeSelect: themeModeSelect});
|
||||||
document.body.dataset.themeMode = nextMode;
|
|
||||||
if (themeModeSelect) themeModeSelect.value = nextMode;
|
|
||||||
if (persist) localStorage.setItem("krishd-theme-mode", nextMode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setSettingsView(view) {
|
function setSettingsView(view) {
|
||||||
@@ -247,12 +226,7 @@ function openPersonaModal(persona = null, personaType = "assistant") {
|
|||||||
settingsPersonaFormTitle.textContent = persona
|
settingsPersonaFormTitle.textContent = persona
|
||||||
? `Edit ${settingsPersonaType.value === "assistant" ? "AI Character" : "persona"}`
|
? `Edit ${settingsPersonaType.value === "assistant" ? "AI Character" : "persona"}`
|
||||||
: `Create ${personaType === "assistant" ? "AI Character" : "persona"}`;
|
: `Create ${personaType === "assistant" ? "AI Character" : "persona"}`;
|
||||||
settingsPersonaName.value = persona?.name || "";
|
populatePersonaForm(settingsPersonaFields, persona || {});
|
||||||
settingsPersonaPronouns.value = persona?.pronouns || "";
|
|
||||||
settingsPersonaAppearance.value = persona?.appearance || "";
|
|
||||||
settingsPersonaBackground.value = persona?.background || "";
|
|
||||||
settingsPersonaDetails.value = persona?.details || "";
|
|
||||||
settingsPersonaExampleDialogues.value = persona?.example_dialogues || "";
|
|
||||||
syncPersonaExamplesField();
|
syncPersonaExamplesField();
|
||||||
setPersonaNotice("");
|
setPersonaNotice("");
|
||||||
settingsPersonaModal.classList.remove("hidden");
|
settingsPersonaModal.classList.remove("hidden");
|
||||||
@@ -359,12 +333,8 @@ async function loadPersonas() {
|
|||||||
async function savePersona() {
|
async function savePersona() {
|
||||||
const wasEditing = Boolean(editingPersonaId);
|
const wasEditing = Boolean(editingPersonaId);
|
||||||
const payload = {
|
const payload = {
|
||||||
|
...readPersonaForm(settingsPersonaFields),
|
||||||
personaType: settingsPersonaType.value,
|
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() : ""
|
exampleDialogues: settingsPersonaType.value === "assistant" ? settingsPersonaExampleDialogues.value.trim() : ""
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -602,8 +572,7 @@ settingsPopupInput.addEventListener("keydown", (event) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener("load", async () => {
|
window.addEventListener("load", async () => {
|
||||||
applyTheme(localStorage.getItem("krishd-theme") || "fakegpt", false);
|
initializeAppearance({title: "Settings", themeSelect: settingsThemeSelect, modeSelect: themeModeSelect});
|
||||||
applyThemeMode(localStorage.getItem("krishd-theme-mode") || "light", false);
|
|
||||||
workspaceModeSelect.value = localStorage.getItem("krishd-workspace-mode") || "basic";
|
workspaceModeSelect.value = localStorage.getItem("krishd-workspace-mode") || "basic";
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const view = params.get("view") || "personal";
|
const view = params.get("view") || "personal";
|
||||||
|
|||||||
+563
-6516
File diff suppressed because it is too large
Load Diff
+78
@@ -1,4 +1,5 @@
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
|
import {randomUUID} from "crypto";
|
||||||
import {performance} from "perf_hooks";
|
import {performance} from "perf_hooks";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import session from "express-session";
|
import session from "express-session";
|
||||||
@@ -70,6 +71,78 @@ function createLifecycleMonitors(app, config) {
|
|||||||
return resources;
|
return resources;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function safeRequestId(value) {
|
||||||
|
const candidate = String(value || "").trim();
|
||||||
|
if (/^[A-Za-z0-9._:-]{8,128}$/.test(candidate)) return candidate;
|
||||||
|
return randomUUID();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createObservability(config) {
|
||||||
|
const counters = {
|
||||||
|
startedAt: new Date().toISOString(),
|
||||||
|
requestsTotal: 0,
|
||||||
|
responsesByStatus: {},
|
||||||
|
errorsTotal: 0,
|
||||||
|
slowRequestsTotal: 0,
|
||||||
|
lastFailure: null
|
||||||
|
};
|
||||||
|
|
||||||
|
const recordError = (req, error, status = 500) => {
|
||||||
|
counters.errorsTotal += 1;
|
||||||
|
counters.lastFailure = {
|
||||||
|
at: new Date().toISOString(),
|
||||||
|
requestId: req?.id || null,
|
||||||
|
method: req?.method || null,
|
||||||
|
path: req?.originalUrl || req?.url || null,
|
||||||
|
status,
|
||||||
|
message: String(error?.message || error || "Unknown error").slice(0, 500)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const middleware = (req, res, next) => {
|
||||||
|
const start = Date.now();
|
||||||
|
req.id = safeRequestId(req.get("X-Request-ID"));
|
||||||
|
res.setHeader("X-Request-ID", req.id);
|
||||||
|
|
||||||
|
res.on("finish", () => {
|
||||||
|
const durationMs = Date.now() - start;
|
||||||
|
const status = res.statusCode;
|
||||||
|
const statusKey = String(status);
|
||||||
|
counters.requestsTotal += 1;
|
||||||
|
counters.responsesByStatus[statusKey] = (counters.responsesByStatus[statusKey] || 0) + 1;
|
||||||
|
if (durationMs > config.slowRequestWarnMs) counters.slowRequestsTotal += 1;
|
||||||
|
if (status >= 500 && counters.lastFailure?.requestId !== req.id) {
|
||||||
|
recordError(req, `HTTP ${status}`, status);
|
||||||
|
}
|
||||||
|
if (config.structuredRequestLoggingEnabled) {
|
||||||
|
console.log(JSON.stringify({
|
||||||
|
type: "request",
|
||||||
|
requestId: req.id,
|
||||||
|
method: req.method,
|
||||||
|
path: req.originalUrl || req.url,
|
||||||
|
status,
|
||||||
|
durationMs,
|
||||||
|
slow: durationMs > config.slowRequestWarnMs,
|
||||||
|
user: req.session?.user || null
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
middleware,
|
||||||
|
recordError,
|
||||||
|
getSnapshot() {
|
||||||
|
return {
|
||||||
|
...counters,
|
||||||
|
responsesByStatus: {...counters.responsesByStatus}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function wantsJson(req) {
|
function wantsJson(req) {
|
||||||
const accept = req.get("Accept") || "";
|
const accept = req.get("Accept") || "";
|
||||||
const contentType = req.get("Content-Type") || "";
|
const contentType = req.get("Content-Type") || "";
|
||||||
@@ -92,11 +165,13 @@ function errorHandler(err, req, res, next) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (err?.type === "entity.parse.failed") {
|
if (err?.type === "entity.parse.failed") {
|
||||||
|
req.app?.locals?.observability?.recordError(req, err, 400);
|
||||||
return res.status(400).json({error: "Invalid JSON"});
|
return res.status(400).json({error: "Invalid JSON"});
|
||||||
}
|
}
|
||||||
|
|
||||||
const status = Number.isInteger(err.status) ? err.status : Number.isInteger(err.statusCode) ? err.statusCode : 500;
|
const status = Number.isInteger(err.status) ? err.status : Number.isInteger(err.statusCode) ? err.statusCode : 500;
|
||||||
const message = status >= 500 ? "Internal server error" : err.message;
|
const message = status >= 500 ? "Internal server error" : err.message;
|
||||||
|
req.app?.locals?.observability?.recordError(req, err, status);
|
||||||
if (status >= 500) {
|
if (status >= 500) {
|
||||||
console.warn("REQUEST ERROR", status, req.method, req.url, err);
|
console.warn("REQUEST ERROR", status, req.method, req.url, err);
|
||||||
} else {
|
} else {
|
||||||
@@ -165,6 +240,7 @@ export function createApp(options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const closeHandlers = createLifecycleMonitors(app, config);
|
const closeHandlers = createLifecycleMonitors(app, config);
|
||||||
|
const observability = createObservability(config);
|
||||||
|
|
||||||
const db = createDatabase(config);
|
const db = createDatabase(config);
|
||||||
const repositories = createRepositories(db, config);
|
const repositories = createRepositories(db, config);
|
||||||
@@ -179,6 +255,7 @@ export function createApp(options = {}) {
|
|||||||
() => db.close()
|
() => db.close()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
app.use(observability.middleware);
|
||||||
app.use(express.json({limit: "1mb"}));
|
app.use(express.json({limit: "1mb"}));
|
||||||
app.use(express.static(config.publicDir, {
|
app.use(express.static(config.publicDir, {
|
||||||
maxAge: config.staticMaxAge,
|
maxAge: config.staticMaxAge,
|
||||||
@@ -204,6 +281,7 @@ export function createApp(options = {}) {
|
|||||||
app.locals.db = db;
|
app.locals.db = db;
|
||||||
app.locals.repositories = repositories;
|
app.locals.repositories = repositories;
|
||||||
app.locals.modelService = modelService;
|
app.locals.modelService = modelService;
|
||||||
|
app.locals.observability = observability;
|
||||||
app.locals.close = createCloseHandler(closeHandlers);
|
app.locals.close = createCloseHandler(closeHandlers);
|
||||||
|
|
||||||
app.use(createAuthRouter({repositories, authRateLimiters, config}));
|
app.use(createAuthRouter({repositories, authRateLimiters, config}));
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export function createConfig(overrides = {}) {
|
|||||||
eventLoopLagWarnMs: int(process.env.EVENT_LOOP_LAG_WARN_MS, 50),
|
eventLoopLagWarnMs: int(process.env.EVENT_LOOP_LAG_WARN_MS, 50),
|
||||||
slowRequestLoggingEnabled: bool(process.env.SLOW_REQUEST_LOGGING_ENABLED, !testMode),
|
slowRequestLoggingEnabled: bool(process.env.SLOW_REQUEST_LOGGING_ENABLED, !testMode),
|
||||||
slowRequestWarnMs: int(process.env.SLOW_REQUEST_WARN_MS, 50),
|
slowRequestWarnMs: int(process.env.SLOW_REQUEST_WARN_MS, 50),
|
||||||
|
structuredRequestLoggingEnabled: bool(process.env.STRUCTURED_REQUEST_LOGGING_ENABLED, !testMode),
|
||||||
chatLimit: int(process.env.CHAT_LIMIT, 10),
|
chatLimit: int(process.env.CHAT_LIMIT, 10),
|
||||||
chatHistoryLimit: int(process.env.CHAT_HISTORY_LIMIT, 20),
|
chatHistoryLimit: int(process.env.CHAT_HISTORY_LIMIT, 20),
|
||||||
chatSummaryUpdateEveryMessages: int(process.env.CHAT_SUMMARY_UPDATE_EVERY_MESSAGES, 8),
|
chatSummaryUpdateEveryMessages: int(process.env.CHAT_SUMMARY_UPDATE_EVERY_MESSAGES, 8),
|
||||||
|
|||||||
@@ -236,6 +236,50 @@ export function createRepositories(db, config) {
|
|||||||
AND username = ?
|
AND username = ?
|
||||||
`),
|
`),
|
||||||
deleteChatMemory: db.prepare("DELETE FROM chat_memories WHERE id = ? AND chat_id = ? AND username = ?"),
|
deleteChatMemory: db.prepare("DELETE FROM chat_memories WHERE id = ? AND chat_id = ? AND username = ?"),
|
||||||
|
getPersonaChatState: db.prepare(`
|
||||||
|
SELECT id,
|
||||||
|
username,
|
||||||
|
chat_id,
|
||||||
|
assistant_persona_id,
|
||||||
|
user_persona_id,
|
||||||
|
relationship_notes,
|
||||||
|
tone,
|
||||||
|
current_location,
|
||||||
|
goals,
|
||||||
|
unresolved_threads,
|
||||||
|
boundaries,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM persona_chat_state
|
||||||
|
WHERE chat_id = ?
|
||||||
|
AND username = ?
|
||||||
|
`),
|
||||||
|
upsertPersonaChatState: db.prepare(`
|
||||||
|
INSERT INTO persona_chat_state (
|
||||||
|
username,
|
||||||
|
chat_id,
|
||||||
|
assistant_persona_id,
|
||||||
|
user_persona_id,
|
||||||
|
relationship_notes,
|
||||||
|
tone,
|
||||||
|
current_location,
|
||||||
|
goals,
|
||||||
|
unresolved_threads,
|
||||||
|
boundaries
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(chat_id) DO UPDATE SET
|
||||||
|
username = excluded.username,
|
||||||
|
assistant_persona_id = excluded.assistant_persona_id,
|
||||||
|
user_persona_id = excluded.user_persona_id,
|
||||||
|
relationship_notes = excluded.relationship_notes,
|
||||||
|
tone = excluded.tone,
|
||||||
|
current_location = excluded.current_location,
|
||||||
|
goals = excluded.goals,
|
||||||
|
unresolved_threads = excluded.unresolved_threads,
|
||||||
|
boundaries = excluded.boundaries,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
`),
|
||||||
updateChatMessage: db.prepare("UPDATE chat_messages SET content = ? WHERE id = ? AND chat_id = ?"),
|
updateChatMessage: db.prepare("UPDATE chat_messages SET content = ? WHERE id = ? AND chat_id = ?"),
|
||||||
updateChatMessageWithRetryState: db.prepare("UPDATE chat_messages SET content = ?, retry_variants = ?, retry_active_index = ?, retry_retries_used = ?, retry_prompt_message_id = ? WHERE id = ? AND chat_id = ?"),
|
updateChatMessageWithRetryState: db.prepare("UPDATE chat_messages SET content = ?, retry_variants = ?, retry_active_index = ?, retry_retries_used = ?, retry_prompt_message_id = ? WHERE id = ? AND chat_id = ?"),
|
||||||
deleteChatMessage: db.prepare("DELETE FROM chat_messages WHERE id = ? AND chat_id = ?"),
|
deleteChatMessage: db.prepare("DELETE FROM chat_messages WHERE id = ? AND chat_id = ?"),
|
||||||
@@ -545,6 +589,7 @@ export function createRepositories(db, config) {
|
|||||||
updatePersonaVersionsUsername: db.prepare("UPDATE persona_versions SET username = ? WHERE username = ?"),
|
updatePersonaVersionsUsername: db.prepare("UPDATE persona_versions SET username = ? WHERE username = ?"),
|
||||||
updatePromptTemplatesUsername: db.prepare("UPDATE prompt_templates SET username = ? WHERE username = ?"),
|
updatePromptTemplatesUsername: db.prepare("UPDATE prompt_templates SET username = ? WHERE username = ?"),
|
||||||
updateChatMemoriesUsername: db.prepare("UPDATE chat_memories SET username = ? WHERE username = ?"),
|
updateChatMemoriesUsername: db.prepare("UPDATE chat_memories SET username = ? WHERE username = ?"),
|
||||||
|
updatePersonaChatStateUsername: db.prepare("UPDATE persona_chat_state SET username = ? WHERE username = ?"),
|
||||||
updateMarketFavoritesUsername: db.prepare("UPDATE persona_market_favorites SET username = ? WHERE username = ?"),
|
updateMarketFavoritesUsername: db.prepare("UPDATE persona_market_favorites SET username = ? WHERE username = ?"),
|
||||||
updateMarketRatingsUsername: db.prepare("UPDATE persona_market_ratings SET username = ? WHERE username = ?"),
|
updateMarketRatingsUsername: db.prepare("UPDATE persona_market_ratings SET username = ? WHERE username = ?"),
|
||||||
updateMarketReportsUsername: db.prepare("UPDATE persona_market_reports SET reporter_username = ? WHERE reporter_username = ?"),
|
updateMarketReportsUsername: db.prepare("UPDATE persona_market_reports SET reporter_username = ? WHERE reporter_username = ?"),
|
||||||
@@ -558,6 +603,7 @@ export function createRepositories(db, config) {
|
|||||||
deleteMarketFavoritesByUser: db.prepare("DELETE FROM persona_market_favorites WHERE username = ?"),
|
deleteMarketFavoritesByUser: db.prepare("DELETE FROM persona_market_favorites WHERE username = ?"),
|
||||||
deleteMarketRatingsByUser: db.prepare("DELETE FROM persona_market_ratings WHERE username = ?"),
|
deleteMarketRatingsByUser: db.prepare("DELETE FROM persona_market_ratings WHERE username = ?"),
|
||||||
deleteMarketReportsByUser: db.prepare("DELETE FROM persona_market_reports WHERE reporter_username = ?"),
|
deleteMarketReportsByUser: db.prepare("DELETE FROM persona_market_reports WHERE reporter_username = ?"),
|
||||||
|
deletePersonaChatStateByUser: db.prepare("DELETE FROM persona_chat_state WHERE username = ?"),
|
||||||
deleteUser: db.prepare("DELETE FROM users WHERE username = ?"),
|
deleteUser: db.prepare("DELETE FROM users WHERE username = ?"),
|
||||||
deleteMarketPersonaByPersonaId: db.prepare("DELETE FROM persona_market WHERE persona_id = ? AND creator_username = ?"),
|
deleteMarketPersonaByPersonaId: db.prepare("DELETE FROM persona_market WHERE persona_id = ? AND creator_username = ?"),
|
||||||
incrementMarketUsageCount: db.prepare("UPDATE persona_market SET usage_count = usage_count + 1 WHERE id = ?")
|
incrementMarketUsageCount: db.prepare("UPDATE persona_market SET usage_count = usage_count + 1 WHERE id = ?")
|
||||||
@@ -573,6 +619,7 @@ export function createRepositories(db, config) {
|
|||||||
statements.updatePersonaVersionsUsername.run(nextUsername, currentUsername);
|
statements.updatePersonaVersionsUsername.run(nextUsername, currentUsername);
|
||||||
statements.updatePromptTemplatesUsername.run(nextUsername, currentUsername);
|
statements.updatePromptTemplatesUsername.run(nextUsername, currentUsername);
|
||||||
statements.updateChatMemoriesUsername.run(nextUsername, currentUsername);
|
statements.updateChatMemoriesUsername.run(nextUsername, currentUsername);
|
||||||
|
statements.updatePersonaChatStateUsername.run(nextUsername, currentUsername);
|
||||||
statements.updateMarketFavoritesUsername.run(nextUsername, currentUsername);
|
statements.updateMarketFavoritesUsername.run(nextUsername, currentUsername);
|
||||||
statements.updateMarketRatingsUsername.run(nextUsername, currentUsername);
|
statements.updateMarketRatingsUsername.run(nextUsername, currentUsername);
|
||||||
statements.updateMarketReportsUsername.run(nextUsername, currentUsername);
|
statements.updateMarketReportsUsername.run(nextUsername, currentUsername);
|
||||||
@@ -586,6 +633,7 @@ export function createRepositories(db, config) {
|
|||||||
statements.deletePromptTemplatesByUser.run(username);
|
statements.deletePromptTemplatesByUser.run(username);
|
||||||
statements.deletePersonaVersionsByUser.run(username);
|
statements.deletePersonaVersionsByUser.run(username);
|
||||||
statements.deleteChatMemoriesByUser.run(username);
|
statements.deleteChatMemoriesByUser.run(username);
|
||||||
|
statements.deletePersonaChatStateByUser.run(username);
|
||||||
statements.deleteChatSessionsByUser.run(username);
|
statements.deleteChatSessionsByUser.run(username);
|
||||||
statements.deleteUserSettingsByUser.run(username);
|
statements.deleteUserSettingsByUser.run(username);
|
||||||
statements.deletePersonasByUser.run(username);
|
statements.deletePersonasByUser.run(username);
|
||||||
@@ -686,6 +734,21 @@ export function createRepositories(db, config) {
|
|||||||
if (!fact) continue;
|
if (!fact) continue;
|
||||||
statements.insertChatMemory.run(username, chatId, null, null, fact);
|
statements.insertChatMemory.run(username, chatId, null, null, fact);
|
||||||
}
|
}
|
||||||
|
const personaState = chat.persona_state || chat.personaChatState;
|
||||||
|
if (personaState) {
|
||||||
|
statements.upsertPersonaChatState.run(
|
||||||
|
username,
|
||||||
|
chatId,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
personaState.relationship_notes ?? personaState.relationshipNotes ?? null,
|
||||||
|
personaState.tone ?? null,
|
||||||
|
personaState.current_location ?? personaState.currentLocation ?? null,
|
||||||
|
personaState.goals ?? null,
|
||||||
|
personaState.unresolved_threads ?? personaState.unresolvedThreads ?? null,
|
||||||
|
personaState.boundaries ?? null
|
||||||
|
);
|
||||||
|
}
|
||||||
imported.chats += 1;
|
imported.chats += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -727,6 +790,22 @@ export function createRepositories(db, config) {
|
|||||||
updateChatTitle: (chatId, username, title) => statements.updateChatSessionTitle.run(title, chatId, username),
|
updateChatTitle: (chatId, username, title) => statements.updateChatSessionTitle.run(title, chatId, username),
|
||||||
updateChatScene: (chatId, username, scenarioPrompt, scenarioSummary) => statements.updateChatSessionScene.run(scenarioPrompt, scenarioSummary, chatId, username),
|
updateChatScene: (chatId, username, scenarioPrompt, scenarioSummary) => statements.updateChatSessionScene.run(scenarioPrompt, scenarioSummary, chatId, username),
|
||||||
updateChatContextSummary: (chatId, username, summary, messageId) => statements.updateChatContextSummary.run(summary, Number(messageId || 0), chatId, username),
|
updateChatContextSummary: (chatId, username, summary, messageId) => statements.updateChatContextSummary.run(summary, Number(messageId || 0), chatId, username),
|
||||||
|
getPersonaChatState: (chatId, username) => statements.getPersonaChatState.get(chatId, username) || null,
|
||||||
|
updatePersonaChatState(username, chat, state) {
|
||||||
|
statements.upsertPersonaChatState.run(
|
||||||
|
username,
|
||||||
|
chat.id,
|
||||||
|
chat.assistant_persona_id ?? null,
|
||||||
|
chat.user_persona_id ?? null,
|
||||||
|
state.relationship_notes ?? null,
|
||||||
|
state.tone ?? null,
|
||||||
|
state.current_location ?? null,
|
||||||
|
state.goals ?? null,
|
||||||
|
state.unresolved_threads ?? null,
|
||||||
|
state.boundaries ?? null
|
||||||
|
);
|
||||||
|
return statements.getPersonaChatState.get(chat.id, username);
|
||||||
|
},
|
||||||
updateChatOrganization(chatId, username, {folderName = null, isPinned = false, archivedAt = null}) {
|
updateChatOrganization(chatId, username, {folderName = null, isPinned = false, archivedAt = null}) {
|
||||||
return statements.updateChatSessionOrganization.run(folderName, isPinned ? 1 : 0, archivedAt, chatId, username);
|
return statements.updateChatSessionOrganization.run(folderName, isPinned ? 1 : 0, archivedAt, chatId, username);
|
||||||
},
|
},
|
||||||
@@ -994,6 +1073,7 @@ export function createRepositories(db, config) {
|
|||||||
chats: this.listChats(username).map((chat) => ({
|
chats: this.listChats(username).map((chat) => ({
|
||||||
...chat,
|
...chat,
|
||||||
memories: this.listChatMemories(chat.id, username),
|
memories: this.listChatMemories(chat.id, username),
|
||||||
|
persona_state: this.getPersonaChatState(chat.id, username),
|
||||||
messages: this.listChatMessages(chat.id).map((message) => ({
|
messages: this.listChatMessages(chat.id).map((message) => ({
|
||||||
...message,
|
...message,
|
||||||
retryVariants: parseJsonArray(message.retry_variants)
|
retryVariants: parseJsonArray(message.retry_variants)
|
||||||
|
|||||||
@@ -68,6 +68,29 @@ export function initializeSchema(db) {
|
|||||||
)
|
)
|
||||||
`).run();
|
`).run();
|
||||||
|
|
||||||
|
db.prepare(`
|
||||||
|
CREATE TABLE IF NOT EXISTS persona_chat_state
|
||||||
|
(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT NOT NULL,
|
||||||
|
chat_id INTEGER NOT NULL UNIQUE,
|
||||||
|
assistant_persona_id INTEGER,
|
||||||
|
user_persona_id INTEGER,
|
||||||
|
relationship_notes TEXT,
|
||||||
|
tone TEXT,
|
||||||
|
current_location TEXT,
|
||||||
|
goals TEXT,
|
||||||
|
unresolved_threads TEXT,
|
||||||
|
boundaries TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (username) REFERENCES users (username) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (chat_id) REFERENCES chat_sessions (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (assistant_persona_id) REFERENCES personas (id) ON DELETE SET NULL,
|
||||||
|
FOREIGN KEY (user_persona_id) REFERENCES personas (id) ON DELETE SET NULL
|
||||||
|
)
|
||||||
|
`).run();
|
||||||
|
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
CREATE TABLE IF NOT EXISTS personas
|
CREATE TABLE IF NOT EXISTS personas
|
||||||
(
|
(
|
||||||
@@ -275,6 +298,29 @@ export function runMigrations(db, {dropLegacyChats = true} = {}) {
|
|||||||
)
|
)
|
||||||
`).run();
|
`).run();
|
||||||
|
|
||||||
|
db.prepare(`
|
||||||
|
CREATE TABLE IF NOT EXISTS persona_chat_state
|
||||||
|
(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT NOT NULL,
|
||||||
|
chat_id INTEGER NOT NULL UNIQUE,
|
||||||
|
assistant_persona_id INTEGER,
|
||||||
|
user_persona_id INTEGER,
|
||||||
|
relationship_notes TEXT,
|
||||||
|
tone TEXT,
|
||||||
|
current_location TEXT,
|
||||||
|
goals TEXT,
|
||||||
|
unresolved_threads TEXT,
|
||||||
|
boundaries TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (username) REFERENCES users (username) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (chat_id) REFERENCES chat_sessions (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (assistant_persona_id) REFERENCES personas (id) ON DELETE SET NULL,
|
||||||
|
FOREIGN KEY (user_persona_id) REFERENCES personas (id) ON DELETE SET NULL
|
||||||
|
)
|
||||||
|
`).run();
|
||||||
|
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
CREATE TABLE IF NOT EXISTS persona_versions
|
CREATE TABLE IF NOT EXISTS persona_versions
|
||||||
(
|
(
|
||||||
|
|||||||
@@ -39,6 +39,29 @@ const memoryValidator = (body) => {
|
|||||||
return {value: {fact}};
|
return {value: {fact}};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const personaStateFields = [
|
||||||
|
"relationship_notes",
|
||||||
|
"tone",
|
||||||
|
"current_location",
|
||||||
|
"goals",
|
||||||
|
"unresolved_threads",
|
||||||
|
"boundaries"
|
||||||
|
];
|
||||||
|
|
||||||
|
const normalizeStateText = (value) => String(value ?? "").replace(/\r\n/g, "\n").trim();
|
||||||
|
|
||||||
|
const personaStateValidator = (body) => {
|
||||||
|
const value = {};
|
||||||
|
for (const field of personaStateFields) {
|
||||||
|
const normalized = normalizeStateText(body?.[field]);
|
||||||
|
if (normalized.length > 1200) {
|
||||||
|
return {error: `${field} must be 1200 characters or fewer`};
|
||||||
|
}
|
||||||
|
value[field] = normalized || null;
|
||||||
|
}
|
||||||
|
return {value};
|
||||||
|
};
|
||||||
|
|
||||||
export function createChatsRouter({repositories, chatService, modelService, config, chatRateLimiters}) {
|
export function createChatsRouter({repositories, chatService, modelService, config, chatRateLimiters}) {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
router.use(requireLogin);
|
router.use(requireLogin);
|
||||||
@@ -247,6 +270,22 @@ export function createChatsRouter({repositories, chatService, modelService, conf
|
|||||||
return res.json({message: "Memory deleted"});
|
return res.json({message: "Memory deleted"});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.get("/chats/:id/persona-state", (req, res) => {
|
||||||
|
const chatId = Number(req.params.id);
|
||||||
|
const chat = repositories.getChat(chatId, req.session.user);
|
||||||
|
if (!chat) return res.status(404).json({error: "Chat not found"});
|
||||||
|
return res.json({state: repositories.getPersonaChatState(chatId, req.session.user)});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put("/chats/:id/persona-state", validateBody(personaStateValidator), (req, res) => {
|
||||||
|
const chatId = Number(req.params.id);
|
||||||
|
const chat = repositories.getChat(chatId, req.session.user);
|
||||||
|
if (!chat) return res.status(404).json({error: "Chat not found"});
|
||||||
|
const state = repositories.updatePersonaChatState(req.session.user, chat, req.validatedBody);
|
||||||
|
repositories.touchChat(chatId, req.session.user);
|
||||||
|
return res.json({state});
|
||||||
|
});
|
||||||
|
|
||||||
router.post("/chat", ...chatRateLimiters, validateBody(sendMessageValidator), async (req, res) => {
|
router.post("/chat", ...chatRateLimiters, validateBody(sendMessageValidator), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const result = await chatService.sendChatMessage({
|
const result = await chatService.sendChatMessage({
|
||||||
|
|||||||
+13
-73
@@ -7,52 +7,17 @@ import {
|
|||||||
buildUserPersonaPrompt,
|
buildUserPersonaPrompt,
|
||||||
containsUserVoiceInRoleplayOpener
|
containsUserVoiceInRoleplayOpener
|
||||||
} from "./personaService.js";
|
} from "./personaService.js";
|
||||||
|
import {
|
||||||
|
buildContextSummaryPrompt,
|
||||||
|
buildMemoryPrompt,
|
||||||
|
buildPersonaStatePrompt,
|
||||||
|
clampRetryActiveIndex,
|
||||||
|
formatChatMessage,
|
||||||
|
formatMessagesForSummary,
|
||||||
|
retryStylePrompts
|
||||||
|
} from "./chatMessageHelpers.js";
|
||||||
|
|
||||||
export function createChatService(repositories, modelService, config) {
|
export function createChatService(repositories, modelService, config) {
|
||||||
const retryStylePrompts = {
|
|
||||||
shorter: "Rewrite the assistant reply to be shorter and tighter while preserving its meaning.",
|
|
||||||
direct: "Rewrite the assistant reply to be more direct and less padded.",
|
|
||||||
emotional: "Rewrite the assistant reply to be more emotionally expressive without changing the core meaning.",
|
|
||||||
"stay-in-character": "Rewrite the assistant reply to stay more strongly in character and voice.",
|
|
||||||
dialogue: "Rewrite the assistant reply as dialogue only. Remove narration unless required for clarity."
|
|
||||||
};
|
|
||||||
|
|
||||||
const clampRetryActiveIndex = (index, variants) => {
|
|
||||||
const numeric = Number(index);
|
|
||||||
if (!Number.isInteger(numeric) || numeric < 0) return 0;
|
|
||||||
return Math.min(numeric, Math.max(0, variants.length - 1));
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseRetryVariants = (rawValue, content) => {
|
|
||||||
if (!rawValue) return [content || ""];
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(rawValue);
|
|
||||||
if (Array.isArray(parsed)) {
|
|
||||||
const normalized = parsed.map((item) => String(item || "").trim()).filter(Boolean);
|
|
||||||
if (normalized.length) return normalized;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return [content || ""];
|
|
||||||
}
|
|
||||||
return [content || ""];
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatChatMessage = (row) => {
|
|
||||||
if (!row) return null;
|
|
||||||
const retryVariants = parseRetryVariants(row.retry_variants, row.content);
|
|
||||||
const retryActiveIndex = clampRetryActiveIndex(row.retry_active_index, retryVariants);
|
|
||||||
return {
|
|
||||||
id: row.id,
|
|
||||||
role: row.role,
|
|
||||||
content: row.content,
|
|
||||||
modelName: row.model_name || null,
|
|
||||||
retryVariants,
|
|
||||||
retryActiveIndex,
|
|
||||||
retryRetriesUsed: Number(row.retry_retries_used || 0),
|
|
||||||
retryPromptMessageId: row.retry_prompt_message_id ?? null
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const insertChatMessage = (chatId, role, content, retryState = null, modelName = null) => {
|
const insertChatMessage = (chatId, role, content, retryState = null, modelName = null) => {
|
||||||
const normalizedContent = String(content || "");
|
const normalizedContent = String(content || "");
|
||||||
const payload = retryState
|
const payload = retryState
|
||||||
@@ -87,41 +52,12 @@ export function createChatService(repositories, modelService, config) {
|
|||||||
return repositories.getChatMessageByIndex(chatId, safeIndex) || null;
|
return repositories.getChatMessageByIndex(chatId, safeIndex) || null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildMemoryPrompt = (memories) => {
|
|
||||||
const facts = (memories || [])
|
|
||||||
.map((memory) => String(memory.fact || "").trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
.slice(0, 30);
|
|
||||||
if (!facts.length) return null;
|
|
||||||
return [
|
|
||||||
"LONG-TERM MEMORY:",
|
|
||||||
"These are durable facts the assistant should remember for this chat/persona context.",
|
|
||||||
"Use them for continuity, but do not mention them unless relevant.",
|
|
||||||
...facts.map((fact, index) => `${index + 1}. ${fact}`)
|
|
||||||
].join("\n");
|
|
||||||
};
|
|
||||||
|
|
||||||
const clampSummary = (value) => {
|
const clampSummary = (value) => {
|
||||||
const normalized = String(value || "").trim();
|
const normalized = String(value || "").trim();
|
||||||
if (normalized.length <= config.chatSummaryMaxChars) return normalized;
|
if (normalized.length <= config.chatSummaryMaxChars) return normalized;
|
||||||
return `${normalized.slice(0, Math.max(0, config.chatSummaryMaxChars - 3)).trimEnd()}...`;
|
return `${normalized.slice(0, Math.max(0, config.chatSummaryMaxChars - 3)).trimEnd()}...`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildContextSummaryPrompt = (summary) => {
|
|
||||||
if (!summary) return null;
|
|
||||||
return [
|
|
||||||
"CONVERSATION SUMMARY:",
|
|
||||||
"This is compressed background context from earlier messages in this chat.",
|
|
||||||
"Use it for continuity. Recent raw messages below are more authoritative.",
|
|
||||||
summary
|
|
||||||
].join("\n");
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatMessagesForSummary = (messages) => messages.map((message) => {
|
|
||||||
const speaker = message.role === "bot" ? "Assistant" : message.role === "user" ? "User" : message.role;
|
|
||||||
return `${speaker}: ${String(message.content || "").trim()}`;
|
|
||||||
}).join("\n\n");
|
|
||||||
|
|
||||||
const maybeUpdateContextSummary = async ({user, chatId, selectedModel}) => {
|
const maybeUpdateContextSummary = async ({user, chatId, selectedModel}) => {
|
||||||
if (config.chatSummaryUpdateEveryMessages <= 0) return;
|
if (config.chatSummaryUpdateEveryMessages <= 0) return;
|
||||||
const session = repositories.getChat(chatId, user);
|
const session = repositories.getChat(chatId, user);
|
||||||
@@ -233,6 +169,7 @@ export function createChatService(repositories, modelService, config) {
|
|||||||
const activeUserPersona = repositories.getUserPersonaForChat(chatId, user) || repositories.getActiveUserPersona(user);
|
const activeUserPersona = repositories.getUserPersonaForChat(chatId, user) || repositories.getActiveUserPersona(user);
|
||||||
const promptMessage = repositories.getPreviousUserMessage(chatId, targetMessage.id);
|
const promptMessage = repositories.getPreviousUserMessage(chatId, targetMessage.id);
|
||||||
const memoryPrompt = buildMemoryPrompt(repositories.listChatMemories(chatId, user));
|
const memoryPrompt = buildMemoryPrompt(repositories.listChatMemories(chatId, user));
|
||||||
|
const personaStatePrompt = buildPersonaStatePrompt(repositories.getPersonaChatState(chatId, user));
|
||||||
|
|
||||||
if (!promptMessage) {
|
if (!promptMessage) {
|
||||||
if (!activePersona || !session.scenario_summary) {
|
if (!activePersona || !session.scenario_summary) {
|
||||||
@@ -269,6 +206,7 @@ export function createChatService(repositories, modelService, config) {
|
|||||||
const contextSummaryPrompt = buildContextSummaryPrompt(session.context_summary);
|
const contextSummaryPrompt = buildContextSummaryPrompt(session.context_summary);
|
||||||
if (contextSummaryPrompt) messagesPayload.push({role: "system", content: contextSummaryPrompt});
|
if (contextSummaryPrompt) messagesPayload.push({role: "system", content: contextSummaryPrompt});
|
||||||
if (memoryPrompt) messagesPayload.push({role: "system", content: memoryPrompt});
|
if (memoryPrompt) messagesPayload.push({role: "system", content: memoryPrompt});
|
||||||
|
if (personaStatePrompt) messagesPayload.push({role: "system", content: personaStatePrompt});
|
||||||
messagesPayload.push(...conversation.map((message) => ({
|
messagesPayload.push(...conversation.map((message) => ({
|
||||||
role: message.role === "bot" ? "assistant" : "user",
|
role: message.role === "bot" ? "assistant" : "user",
|
||||||
content: message.content
|
content: message.content
|
||||||
@@ -318,6 +256,8 @@ export function createChatService(repositories, modelService, config) {
|
|||||||
if (contextSummaryPrompt) messagesPayload.push({role: "system", content: contextSummaryPrompt});
|
if (contextSummaryPrompt) messagesPayload.push({role: "system", content: contextSummaryPrompt});
|
||||||
const memoryPrompt = buildMemoryPrompt(repositories.listChatMemories(chatId, user));
|
const memoryPrompt = buildMemoryPrompt(repositories.listChatMemories(chatId, user));
|
||||||
if (memoryPrompt) messagesPayload.push({role: "system", content: memoryPrompt});
|
if (memoryPrompt) messagesPayload.push({role: "system", content: memoryPrompt});
|
||||||
|
const personaStatePrompt = buildPersonaStatePrompt(repositories.getPersonaChatState(chatId, user));
|
||||||
|
if (personaStatePrompt) messagesPayload.push({role: "system", content: personaStatePrompt});
|
||||||
messagesPayload.push(...conversation.map((entry) => ({
|
messagesPayload.push(...conversation.map((entry) => ({
|
||||||
role: entry.role === "bot" ? "assistant" : "user",
|
role: entry.role === "bot" ? "assistant" : "user",
|
||||||
content: entry.content
|
content: entry.content
|
||||||
|
|||||||
@@ -104,6 +104,22 @@ function validateMemory(memory, chatIndex, memoryIndex, errors) {
|
|||||||
return {fact};
|
return {fact};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validatePersonaState(state, chatIndex, errors) {
|
||||||
|
if (state == null) return null;
|
||||||
|
if (!state || typeof state !== "object" || Array.isArray(state)) {
|
||||||
|
errors.push(`Chat ${chatIndex + 1}, persona_state must be an object.`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
relationship_notes: optionalText(state.relationship_notes || state.relationshipNotes),
|
||||||
|
tone: optionalText(state.tone),
|
||||||
|
current_location: optionalText(state.current_location || state.currentLocation),
|
||||||
|
goals: optionalText(state.goals),
|
||||||
|
unresolved_threads: optionalText(state.unresolved_threads || state.unresolvedThreads),
|
||||||
|
boundaries: optionalText(state.boundaries)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function validateChat(chat, index, errors) {
|
function validateChat(chat, index, errors) {
|
||||||
if (!chat || typeof chat !== "object" || Array.isArray(chat)) {
|
if (!chat || typeof chat !== "object" || Array.isArray(chat)) {
|
||||||
errors.push(`Chat ${index + 1} must be an object.`);
|
errors.push(`Chat ${index + 1} must be an object.`);
|
||||||
@@ -112,6 +128,7 @@ function validateChat(chat, index, errors) {
|
|||||||
const title = normalizeText(chat.stored_title || chat.title || "Imported chat") || "Imported chat";
|
const title = normalizeText(chat.stored_title || chat.title || "Imported chat") || "Imported chat";
|
||||||
const messages = asArray(chat.messages).map((message, messageIndex) => validateMessage(message, index, messageIndex, errors)).filter(Boolean);
|
const messages = asArray(chat.messages).map((message, messageIndex) => validateMessage(message, index, messageIndex, errors)).filter(Boolean);
|
||||||
const memories = asArray(chat.memories).map((memory, memoryIndex) => validateMemory(memory, index, memoryIndex, errors)).filter(Boolean);
|
const memories = asArray(chat.memories).map((memory, memoryIndex) => validateMemory(memory, index, memoryIndex, errors)).filter(Boolean);
|
||||||
|
const personaState = validatePersonaState(chat.persona_state || chat.personaChatState, index, errors);
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
stored_title: title,
|
stored_title: title,
|
||||||
@@ -125,7 +142,8 @@ function validateChat(chat, index, errors) {
|
|||||||
? Number(chat.context_summary_message_id || chat.contextSummaryMessageId)
|
? Number(chat.context_summary_message_id || chat.contextSummaryMessageId)
|
||||||
: 0,
|
: 0,
|
||||||
messages,
|
messages,
|
||||||
memories
|
memories,
|
||||||
|
persona_state: personaState
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user