From feaa2e42c49322f9a8483f1ec2662625031b055a Mon Sep 17 00:00:00 2001 From: fretuks Date: Tue, 14 Apr 2026 23:21:01 +0200 Subject: [PATCH] implement an entire base refacotr and alot of styling overhauls and remakes. --- README.md | 76 +- package.json | 5 +- public/app/api.js | 52 + public/app/constants.js | 21 + public/app/dom.js | 226 ++++ public/index.html | 224 +-- public/market.html | 62 +- public/market.js | 223 ++- public/script.js | 523 ++++--- public/settings.html | 33 +- public/settings.js | 107 +- public/style.css | 1313 ++++++++++++++++++ scripts/lint.mjs | 32 + server.js | 2329 +------------------------------- src/app.js | 123 ++ src/config.js | 46 + src/db/index.js | 14 + src/db/repositories.js | 910 +++++++++++++ src/db/schema.js | 336 +++++ src/middleware/auth.js | 6 + src/middleware/rateLimit.js | 28 + src/middleware/validate.js | 10 + src/routes/auth.js | 48 + src/routes/chats.js | 334 +++++ src/routes/pages.js | 11 + src/routes/personas.js | 377 ++++++ src/routes/settings.js | 66 + src/routes/system.js | 12 + src/services/chatService.js | 319 +++++ src/services/modelService.js | 196 +++ src/services/personaService.js | 211 +++ test/app.test.js | 396 ++++++ 32 files changed, 6031 insertions(+), 2638 deletions(-) create mode 100644 public/app/api.js create mode 100644 public/app/constants.js create mode 100644 public/app/dom.js create mode 100644 scripts/lint.mjs create mode 100644 src/app.js create mode 100644 src/config.js create mode 100644 src/db/index.js create mode 100644 src/db/repositories.js create mode 100644 src/db/schema.js create mode 100644 src/middleware/auth.js create mode 100644 src/middleware/rateLimit.js create mode 100644 src/middleware/validate.js create mode 100644 src/routes/auth.js create mode 100644 src/routes/chats.js create mode 100644 src/routes/pages.js create mode 100644 src/routes/personas.js create mode 100644 src/routes/settings.js create mode 100644 src/routes/system.js create mode 100644 src/services/chatService.js create mode 100644 src/services/modelService.js create mode 100644 src/services/personaService.js create mode 100644 test/app.test.js diff --git a/README.md b/README.md index 59593ca..519793a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,78 @@ # KrisHD_AI -AI Frontend for ai.krishd.ch + +KrisHD_AI is an Express + SQLite app for account-based chat, persona management, roleplay chat sessions, and a simple persona market backed by a remote model API. + +## Setup + +1. Install dependencies with `npm install`. +2. Copy `.env.example` to `.env` and set at least `SESSION_SECRET`. +3. Start the app with `npm run dev` for local development or `npm start` for a normal server run. + +## Scripts + +- `npm start`: run the production server entrypoint. +- `npm run dev`: run the server with Node watch mode. +- `npm run lint`: syntax-check the backend and browser modules. +- `npm test`: run API tests with Node's built-in test runner. + +## Environment Variables + +- `PORT`: HTTP port for the Express server. +- `DB_PATH`: SQLite database path. Use `:memory:` for isolated tests. +- `SESSION_SECRET`: session signing secret. Required outside local throwaway environments. +- `COOKIE_SECURE`: set to `true` when serving over HTTPS. +- `SESSION_MAX_AGE_MS`: cookie lifetime. +- `CHAT_LIMIT`: per-account maximum chat sessions. +- `CHAT_HISTORY_LIMIT`: number of recent messages included in model prompts. +- `MODEL_API_BASE_URL`: base URL of the remote model backend. The server expects `/chat` and `/tags`. +- `MODEL_REQUEST_TIMEOUT_MS`: timeout for chat generation requests. +- `MODEL_LIST_TIMEOUT_MS`: timeout for model list requests. +- `MODEL_UNLOAD_AFTER_MS`: delay before an idle model unload is attempted. +- `MODELS_CACHE_TTL_MS`: model list cache TTL. +- `AUTH_RATE_LIMIT_*`: rate-limit window and thresholds for registration/login. +- `CHAT_RATE_LIMIT_*`: rate-limit window and thresholds for chat and retry endpoints. +- `DROP_LEGACY_CHATS`: when `true`, migrate any legacy `chats` table into `chat_sessions` / `chat_messages` and remove the old table. + +## Model Backend Expectations + +The server is configured around an Ollama-compatible API surface: + +- `POST /chat`: accepts `{ model, messages, stream: true }` and returns newline-delimited JSON stream chunks with `message.content`. +- `GET /tags`: returns the available model list. + +The refactor adds explicit upstream timeout handling, cached model listing, and `GET /health` for degraded-state visibility. + +## Schema Notes + +Primary runtime tables are: + +- `users` +- `chat_sessions` +- `chat_messages` +- `personas` +- `user_settings` +- `persona_market` + +Legacy `chats` migration is handled during startup. New code only reads the session/message schema. + +## Persona And Market Flow + +- Assistant and user personas are stored separately through `persona_type`. +- Roleplay chats can bind an assistant persona, an optional user persona, and scenario metadata to a chat session. +- Publishing copies a local persona into `persona_market`. +- Collecting clones a market persona back into the current user's `personas` table with source metadata. + +## Frontend Structure + +The main browser entrypoint is still `public/script.js`, but shared API/bootstrap concerns are now split into `public/app/` modules: + +- `api.js`: JSON request helpers +- `constants.js`: theme and onboarding constants +- `dom.js`: cached DOM references + +This keeps the next round of UI extraction lower-risk without changing the app's behavior surface. ## Legal + - [Privacy Policy](PRIVACY_POLICY.md) -- [Terms of Service](TERMS_OF_SERVICE.md) \ No newline at end of file +- [Terms of Service](TERMS_OF_SERVICE.md) diff --git a/package.json b/package.json index c511c39..2e0073f 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,10 @@ "type": "module", "main": "server.js", "scripts": { - "start": "node server.js" + "start": "node server.js", + "dev": "node --watch server.js", + "lint": "node scripts/lint.mjs", + "test": "node --test" }, "dependencies": { "bcrypt": "^5.1.1", diff --git a/public/app/api.js b/public/app/api.js new file mode 100644 index 0000000..7446eae --- /dev/null +++ b/public/app/api.js @@ -0,0 +1,52 @@ +export async function request(url, data, method = "POST") { + try { + const res = await fetch(url, { + method, + headers: {"Content-Type": "application/json"}, + body: data ? JSON.stringify(data) : undefined + }); + return await res.json(); + } catch { + return {error: "Network error - please try again."}; + } +} + +export const get = (url) => request(url, null, "GET"); +export const post = (url, data) => request(url, data, "POST"); +export const put = (url, data) => request(url, data, "PUT"); +export const del = (url) => request(url, null, "DELETE"); + +export async function stream(url, data, handlers = {}) { + const response = await fetch(url, { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify(data || {}) + }); + + if (!response.ok || !response.body) { + throw new Error("Streaming request failed"); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + + while (true) { + const {value, done} = await reader.read(); + if (done) break; + buffer += decoder.decode(value, {stream: true}); + const parts = buffer.split("\n\n"); + buffer = parts.pop() || ""; + for (const part of parts) { + const lines = part.split("\n"); + const eventLine = lines.find((line) => line.startsWith("event:")); + const dataLine = lines.find((line) => line.startsWith("data:")); + if (!eventLine || !dataLine) continue; + const event = eventLine.slice(6).trim(); + const payload = JSON.parse(dataLine.slice(5).trim()); + if (event === "chunk" && handlers.onChunk) handlers.onChunk(payload); + if (event === "done" && handlers.onDone) handlers.onDone(payload); + if (event === "error" && handlers.onError) handlers.onError(payload); + } + } +} diff --git a/public/app/constants.js b/public/app/constants.js new file mode 100644 index 0000000..478d154 --- /dev/null +++ b/public/app/constants.js @@ -0,0 +1,21 @@ +export const requestedChatId = Number(new URLSearchParams(window.location.search).get("chat")) || null; + +export const onboardingPrompts = { + ask: ["Explain this topic in simple terms.", "Compare these options and recommend one."], + brainstorm: ["Give me 10 practical ideas for this problem.", "Suggest 3 creative directions and tradeoffs."], + roleplay: ["Start a roleplay scene with immediate tension.", "Give me a dramatic opening with clear stakes."] +}; + +export const themes = { + "fakegpt": {name: "FakeGPT", short: "FG"}, + "fraud": {name: "Fraud", short: "FR"}, + "germini": {name: "Germini", short: "GE"}, + "slopilot": {name: "Slopilot", short: "SP"}, + "beta-ai": {name: "Beta AI", short: "BA"}, + "confusity": {name: "Confusity", short: "CF"} +}; + +export const defaultModelProfile = { + badge: "General", + summary: "General-purpose chat model. Start here if you are unsure which model to use." +}; diff --git a/public/app/dom.js b/public/app/dom.js new file mode 100644 index 0000000..f5d4b94 --- /dev/null +++ b/public/app/dom.js @@ -0,0 +1,226 @@ +const $ = (id) => document.getElementById(id); + +const authDiv = $("auth"); +const chatDiv = $("chat"); +const authMsg = $("authMsg"); +const logoutButton = $("logout"); +const messagesDiv = $("messages"); +const modelSelect = $("model"); +const modelMenuButton = $("modelMenuButton"); +const modelPopover = $("modelPopover"); +const modelBadgeName = $("modelBadgeName"); +const modelCount = $("modelCount"); +const modelHelpTitle = $("modelHelpTitle"); +const modelHelpBadge = $("modelHelpBadge"); +const modelHelpSummary = $("modelHelpSummary"); +const loginForm = $("loginForm"); +const registerForm = $("registerForm"); +const msgInput = $("msgInput"); +const sendBtn = $("send"); +const loginUsernameInput = $("loginUsername"); +const loginPasswordInput = $("loginPassword"); +const registerUsernameInput = $("registerUsername"); +const registerPasswordInput = $("registerPassword"); +const loginSubmit = $("loginSubmit"); +const registerSubmit = $("registerSubmit"); +const chatList = $("chatList"); +const chatSearchInput = $("chatSearch"); +const chatSidebar = $("chatSidebar"); +const chatDrawerCloseBtn = $("chatDrawerClose"); +const newChatBtn = $("newChat"); +const chatListLoading = $("chatListLoading"); +const chatListLoadingText = $("chatListLoadingText"); +const renameChatBtn = $("renameChat"); +const clearChatBtn = $("clearChat"); +const exportChatBtn = $("exportChat"); +const pinChatBtn = $("pinChat"); +const moveChatFolderBtn = $("moveChatFolder"); +const archiveChatBtn = $("archiveChat"); +const backupWorkspaceBtn = $("backupWorkspace"); +const activeChatTitle = $("activeChatTitle"); +const sessionUser = $("sessionUser"); +const chatActivityOverlay = $("chatActivityOverlay"); +const chatActivityEyebrow = $("chatActivityEyebrow"); +const chatActivityTitle = $("chatActivityTitle"); +const chatActivityDetail = $("chatActivityDetail"); +const contextChipButton = $("contextChipButton"); +const contextPopover = $("contextPopover"); +const contextSummaryLabel = $("contextSummaryLabel"); +const chatActionsMenuButton = $("chatActionsMenuButton"); +const chatActionsPopover = $("chatActionsPopover"); +const chatModePill = $("chatModePill"); +const chatCharacterPill = $("chatCharacterPill"); +const chatUserPersonaPill = $("chatUserPersonaPill"); +const personaList = $("personaList"); +const userPersonaList = $("userPersonaList"); +const personaForm = $("personaForm"); +const personaFormTitle = $("personaFormTitle"); +const personaTypeSelect = $("personaType"); +const personaNameInput = $("personaName"); +const personaPronounsInput = $("personaPronouns"); +const personaAppearanceInput = $("personaAppearance"); +const personaBackgroundInput = $("personaBackground"); +const personaDetailsInput = $("personaDetails"); +const personaExamplesField = $("personaExamplesField"); +const personaExampleDialoguesInput = $("personaExampleDialogues"); +const personaFormNotice = $("personaFormNotice"); +const roleplayNewPersonaBtn = $("roleplayNewPersona"); +const clearUserPersonaBtn = $("clearUserPersona"); +const activePersonaStatus = $("activePersonaStatus"); +const activeUserPersonaStatus = $("activeUserPersonaStatus"); +const personaModal = $("personaModal"); +const personaCloseBtn = $("personaClose"); +const personaMenuButton = $("personaMenuButton"); +const personaPopover = $("personaPopover"); +const roleplayStarterModal = $("roleplayStarterModal"); +const roleplayStarterClose = $("roleplayStarterClose"); +const roleplayStarterCancel = $("roleplayStarterCancel"); +const roleplayStarterConfirm = $("roleplayStarterConfirm"); +const roleplayStarterNotice = $("roleplayStarterNotice"); +const roleplayCharacterSelect = $("roleplayCharacterSelect"); +const roleplayUserPersonaSelect = $("roleplayUserPersonaSelect"); +const roleplayStarterTitle = $("roleplayStarterTitle"); +const roleplayStarterIntroCopy = $("roleplayStarterIntroCopy"); +const roleplayStarterBack = $("roleplayStarterBack"); +const roleplayStarterStepOne = $("roleplayStarterStepOne"); +const roleplayStarterStepTwo = $("roleplayStarterStepTwo"); +const roleplayScenarioInput = $("roleplayScenarioInput"); +const roleplayStarterSuggestions = $("roleplayStarterSuggestions"); +const popupModal = $("popupModal"); +const popupCloseBtn = $("popupClose"); +const popupCancelBtn = $("popupCancel"); +const popupConfirmBtn = $("popupConfirm"); +const popupTitle = $("popupTitle"); +const popupEyebrow = $("popupEyebrow"); +const popupDescription = $("popupDescription"); +const popupField = $("popupField"); +const popupInputLabel = $("popupInputLabel"); +const popupInput = $("popupInput"); +const appNotice = $("appNotice"); +const onboardingModal = $("onboardingModal"); +const onboardingTitle = $("onboardingTitle"); +const onboardingSubtitle = $("onboardingSubtitle"); +const onboardingChoices = $("onboardingChoices"); +const onboardingBack = $("onboardingBack"); +const onboardingContinue = $("onboardingContinue"); +const onboardingSkip = $("onboardingSkip"); +const authScreens = document.querySelectorAll(".auth-screen"); +const toggleButtons = document.querySelectorAll(".auth-toggle .toggle"); +const themeNameTargets = document.querySelectorAll("[data-theme-name]"); +const themeLogoTargets = document.querySelectorAll("[data-theme-logo]"); +const modelMenu = document.querySelector(".model-menu"); +const personaMenu = document.querySelector(".persona-menu"); + +export { + $, + activeChatTitle, + activePersonaStatus, + activeUserPersonaStatus, + appNotice, + authDiv, + authMsg, + authScreens, + chatActionsMenuButton, + chatActionsPopover, + chatActivityDetail, + chatActivityEyebrow, + chatActivityOverlay, + chatActivityTitle, + chatCharacterPill, + chatDiv, + chatList, + chatListLoading, + chatListLoadingText, + chatSidebar, + chatDrawerCloseBtn, + chatModePill, + chatSearchInput, + chatUserPersonaPill, + clearChatBtn, + clearUserPersonaBtn, + contextChipButton, + contextPopover, + contextSummaryLabel, + exportChatBtn, + pinChatBtn, + moveChatFolderBtn, + archiveChatBtn, + backupWorkspaceBtn, + loginForm, + loginPasswordInput, + loginSubmit, + loginUsernameInput, + logoutButton, + messagesDiv, + modelBadgeName, + modelCount, + modelHelpBadge, + modelHelpSummary, + modelHelpTitle, + modelMenu, + modelMenuButton, + modelPopover, + modelSelect, + msgInput, + newChatBtn, + onboardingBack, + onboardingChoices, + onboardingContinue, + onboardingModal, + onboardingSkip, + onboardingSubtitle, + onboardingTitle, + personaCloseBtn, + personaDetailsInput, + personaExampleDialoguesInput, + personaExamplesField, + personaForm, + personaFormNotice, + personaFormTitle, + personaList, + personaMenu, + personaMenuButton, + personaModal, + personaNameInput, + personaPopover, + personaPronounsInput, + personaTypeSelect, + personaAppearanceInput, + personaBackgroundInput, + popupCancelBtn, + popupCloseBtn, + popupConfirmBtn, + popupDescription, + popupEyebrow, + popupField, + popupInput, + popupInputLabel, + popupModal, + popupTitle, + registerForm, + registerPasswordInput, + registerSubmit, + registerUsernameInput, + renameChatBtn, + roleplayCharacterSelect, + roleplayNewPersonaBtn, + roleplayScenarioInput, + roleplayStarterBack, + roleplayStarterCancel, + roleplayStarterClose, + roleplayStarterConfirm, + roleplayStarterIntroCopy, + roleplayStarterModal, + roleplayStarterNotice, + roleplayStarterStepOne, + roleplayStarterStepTwo, + roleplayStarterSuggestions, + roleplayStarterTitle, + roleplayUserPersonaSelect, + sendBtn, + sessionUser, + themeLogoTargets, + themeNameTargets, + toggleButtons, + userPersonaList +}; diff --git a/public/index.html b/public/index.html index bdcb00a..9b5f92a 100644 --- a/public/index.html +++ b/public/index.html @@ -16,13 +16,13 @@
-

Private chat

+

Private workspace

FakeGPT

-

Sign in to start a chat immediately, then add personas when you want richer roleplay.

+

Private chat and roleplay.

@@ -75,44 +75,33 @@ -