From 16b7a69d52c28ed1b94327c3fdafc11284e5746b Mon Sep 17 00:00:00 2001 From: fretuks Date: Sat, 5 Sep 2026 01:31:41 +0200 Subject: [PATCH] add missing files --- public/app/personaForm.js | 19 ++++++ public/app/statusNotice.js | 9 +++ public/app/theme-bootstrap.js | 14 +++++ public/app/themeController.js | 60 +++++++++++++++++++ src/services/chatMessageHelpers.js | 96 ++++++++++++++++++++++++++++++ 5 files changed, 198 insertions(+) create mode 100644 public/app/personaForm.js create mode 100644 public/app/statusNotice.js create mode 100644 public/app/theme-bootstrap.js create mode 100644 public/app/themeController.js create mode 100644 src/services/chatMessageHelpers.js diff --git a/public/app/personaForm.js b/public/app/personaForm.js new file mode 100644 index 0000000..0e07577 --- /dev/null +++ b/public/app/personaForm.js @@ -0,0 +1,19 @@ +const personaFieldNames = ["name", "pronouns", "appearance", "background", "details", "exampleDialogues"]; + +export function populatePersonaForm(fields, persona = {}) { + personaFieldNames.forEach((name) => { + const field = fields[name]; + if (!field) return; + const sourceName = name === "exampleDialogues" ? "example_dialogues" : name; + field.value = persona[sourceName] || ""; + }); +} + +export function readPersonaForm(fields) { + return personaFieldNames.reduce((payload, name) => { + const field = fields[name]; + if (!field) return payload; + payload[name] = field.value.trim(); + return payload; + }, {}); +} diff --git a/public/app/statusNotice.js b/public/app/statusNotice.js new file mode 100644 index 0000000..f25bb42 --- /dev/null +++ b/public/app/statusNotice.js @@ -0,0 +1,9 @@ +export function setStatus(element, message = "", state = "", baseClass = "status") { + if (!element) return; + element.textContent = message; + element.className = [baseClass, state].filter(Boolean).join(" "); +} + +export function clearStatus(element, baseClass = "status") { + setStatus(element, "", "hidden", baseClass); +} diff --git a/public/app/theme-bootstrap.js b/public/app/theme-bootstrap.js new file mode 100644 index 0000000..105c081 --- /dev/null +++ b/public/app/theme-bootstrap.js @@ -0,0 +1,14 @@ +(() => { + const themes = new Set(["fakegpt", "fraud", "germini", "slopilot", "beta-ai", "confusity"]); + const storedTheme = localStorage.getItem("krishd-theme"); + const storedMode = localStorage.getItem("krishd-theme-mode"); + const theme = themes.has(storedTheme) ? storedTheme : "fakegpt"; + const mode = storedMode === "dark" ? "dark" : "light"; + document.documentElement.dataset.theme = theme; + document.documentElement.dataset.themeMode = mode; + document.addEventListener("readystatechange", () => { + if (!document.body) return; + document.body.dataset.theme = theme; + document.body.dataset.themeMode = mode; + }); +})(); diff --git a/public/app/themeController.js b/public/app/themeController.js new file mode 100644 index 0000000..aae6d62 --- /dev/null +++ b/public/app/themeController.js @@ -0,0 +1,60 @@ +import {themes} from "./constants.js"; + +export const THEME_STORAGE_KEY = "krishd-theme"; +export const THEME_MODE_STORAGE_KEY = "krishd-theme-mode"; +export const DEFAULT_THEME = "fakegpt"; +export const DEFAULT_THEME_MODE = "light"; + +export function normalizeTheme(themeKey) { + return Object.hasOwn(themes, themeKey) ? themeKey : DEFAULT_THEME; +} + +export function normalizeThemeMode(modeKey) { + return modeKey === "dark" ? "dark" : DEFAULT_THEME_MODE; +} + +export function readStoredAppearance() { + return { + theme: normalizeTheme(localStorage.getItem(THEME_STORAGE_KEY)), + mode: normalizeThemeMode(localStorage.getItem(THEME_MODE_STORAGE_KEY)) + }; +} + +export function applyTheme(themeKey, { + persist = true, + title = null, + themeSelect = null, + nameTargets = document.querySelectorAll("[data-theme-name]"), + logoTargets = document.querySelectorAll("[data-theme-logo]") +} = {}) { + const nextTheme = normalizeTheme(themeKey); + const theme = themes[nextTheme]; + document.body.dataset.theme = nextTheme; + if (typeof title === "function") document.title = title(theme); + if (typeof title === "string") document.title = title; + nameTargets.forEach((target) => { target.textContent = theme.name; }); + logoTargets.forEach((target) => { target.textContent = theme.short; }); + if (themeSelect) themeSelect.value = nextTheme; + if (persist) localStorage.setItem(THEME_STORAGE_KEY, nextTheme); + document.dispatchEvent(new CustomEvent("appearancechange", {detail: {theme: nextTheme}})); + return nextTheme; +} + +export function applyThemeMode(modeKey, {persist = true, modeSelect = null} = {}) { + const nextMode = normalizeThemeMode(modeKey); + document.body.dataset.themeMode = nextMode; + if (modeSelect) modeSelect.value = nextMode; + if (persist) localStorage.setItem(THEME_MODE_STORAGE_KEY, nextMode); + document.dispatchEvent(new CustomEvent("appearancechange", {detail: {mode: nextMode}})); + return nextMode; +} + +export function initializeAppearance(options = {}) { + const appearance = readStoredAppearance(); + applyTheme(appearance.theme, {...options, persist: false}); + applyThemeMode(appearance.mode, { + persist: false, + modeSelect: options.modeSelect || null + }); + return appearance; +} diff --git a/src/services/chatMessageHelpers.js b/src/services/chatMessageHelpers.js new file mode 100644 index 0000000..956fc47 --- /dev/null +++ b/src/services/chatMessageHelpers.js @@ -0,0 +1,96 @@ +export const retryStylePrompts = Object.freeze({ + 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." +}); + +export function 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)); +} + +export function 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 || ""]; +} + +export function 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 + }; +} + +export function 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"); +} + +export function 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"); +} + +export function buildPersonaStatePrompt(state) { + if (!state) return null; + const lines = [ + ["Relationship notes", state.relationship_notes], + ["Tone", state.tone], + ["Current location", state.current_location], + ["Goals", state.goals], + ["Unresolved threads", state.unresolved_threads], + ["Boundaries", state.boundaries] + ] + .map(([label, value]) => [label, String(value || "").trim()]) + .filter(([, value]) => value) + .map(([label, value]) => `${label}: ${value}`); + if (!lines.length) return null; + return [ + "PERSONA RELATIONSHIP STATE:", + "Use this roleplay continuity state for the current assistant/user persona pair.", + "Keep it consistent unless the latest user message clearly changes it.", + ...lines + ].join("\n"); +} + +export function formatMessagesForSummary(messages) { + return messages.map((message) => { + const speaker = message.role === "bot" ? "Assistant" : message.role === "user" ? "User" : message.role; + return `${speaker}: ${String(message.content || "").trim()}`; + }).join("\n\n"); +}