diff --git a/env.go b/env.go index 1db6f8f..c5c8867 100644 --- a/env.go +++ b/env.go @@ -24,6 +24,10 @@ type EnvSettings struct { ImageGeneration bool `json:"image-generation"` } +type EnvUI struct { + ReducedMotion bool `json:"reduced-motion"` +} + type EnvUser struct { ID string `json:"id"` Username string `json:"username"` @@ -41,6 +45,7 @@ type Environment struct { Debug bool `json:"debug"` Tokens EnvTokens `json:"tokens"` Settings EnvSettings `json:"settings"` + UI EnvUI `json:"ui"` Authentication EnvAuthentication `json:"authentication"` } @@ -165,6 +170,8 @@ func (e *Environment) Store() error { "$.settings.title-model": {yaml.HeadComment(" model used to generate titles (needs to have structured output support; default: google/gemini-2.5-flash-lite)")}, "$.settings.image-generation": {yaml.HeadComment(" allow image generation (optional; default: true)")}, + "$.ui.reduced-motion": {yaml.HeadComment(" disables things like the floating stars in the background (optional; default: false)")}, + "$.authentication.enabled": {yaml.HeadComment(" require login with username and password")}, "$.authentication.users": {yaml.HeadComment(" list of users with bcrypt password hashes")}, } diff --git a/example.config.yml b/example.config.yml index aa408d8..8e703f5 100644 --- a/example.config.yml +++ b/example.config.yml @@ -16,6 +16,12 @@ settings: cleanup: true # model used to generate titles (needs to have structured output support; default: google/gemini-2.5-flash-lite) title-model: google/gemini-2.5-flash-lite + # allow image generation (optional; default: true) + image-generation: true + +ui: + # disables things like the floating stars in the background (optional; default: false) + reduced-motion: false authentication: # require login with username and password diff --git a/main.go b/main.go index 1436979..9e14ad9 100644 --- a/main.go +++ b/main.go @@ -36,13 +36,16 @@ func main() { r.Get("/-/data", func(w http.ResponseWriter, r *http.Request) { RespondJson(w, http.StatusOK, map[string]any{ - "authentication": env.Authentication.Enabled, - "authenticated": IsAuthenticated(r), - "search": env.Tokens.Exa != "", - "icons": icons, - "models": models, - "prompts": Prompts, - "version": Version, + "authenticated": IsAuthenticated(r), + "config": map[string]any{ + "auth": env.Authentication.Enabled, + "search": env.Tokens.Exa != "", + "motion": env.UI.ReducedMotion, + }, + "icons": icons, + "models": models, + "prompts": Prompts, + "version": Version, }) }) diff --git a/static/js/chat.js b/static/js/chat.js index 77c1947..09d40e3 100644 --- a/static/js/chat.js +++ b/static/js/chat.js @@ -1358,6 +1358,59 @@ $authentication.classList.add("open"); } + function initFloaters() { + const $floaters = document.getElementById("floaters"), + colors = ["#8aadf4", "#c6a0f6", "#8bd5ca", "#91d7e3", "#b7bdf8"], + count = Math.floor((window.outerHeight * window.outerWidth) / 98000); + + function rand(a, b, rnd = false) { + const num = Math.random() * (b - a) + a; + + if (rnd) { + return Math.floor(num); + } + + return num; + } + + function place(el, init = false) { + el.style.setProperty("--x", `${rand(0, 100).toFixed(4)}vw`); + el.style.setProperty("--y", `${rand(0, 100).toFixed(4)}vh`); + + if (init) { + return; + } + + const time = rand(140, 160); + + el.style.setProperty("--time", `${time.toFixed(2)}s`); + + setTimeout(() => { + place(el); + }, time * 1000); + } + + for (let i = 0; i < count; i++) { + const el = document.createElement("div"); + + el.className = "floater"; + + el.style.setProperty("--size", `${rand(2, 4, true)}px`); + el.style.setProperty("--color", colors[rand(0, colors.length, true)]); + + $floaters.appendChild(el); + + place(el, true); + + setTimeout( + () => { + place(el); + }, + rand(0, 1000, true) + ); + } + } + async function loadData() { const [_, data] = await Promise.all([connectDB(), json("/-/data")]); @@ -1383,10 +1436,15 @@ } // update search availability - searchAvailable = data.search; + searchAvailable = data.config.search; + + // initialize floaters (unless disabled) + if (!data.config.motion) { + initFloaters(); + } // show login modal - if (data.authentication && !data.authenticated) { + if (data.config.authentication && !data.authenticated) { $authentication.classList.add("open"); } @@ -2022,56 +2080,3 @@ document.body.classList.remove("loading"); }); })(); - -(() => { - const $floaters = document.getElementById("floaters"), - colors = ["#8aadf4", "#c6a0f6", "#8bd5ca", "#91d7e3", "#b7bdf8"], - count = Math.floor((window.outerHeight * window.outerWidth) / 98000); - - function rand(a, b, rnd = false) { - const num = Math.random() * (b - a) + a; - - if (rnd) { - return Math.floor(num); - } - - return num; - } - - function place(el, init = false) { - el.style.setProperty("--x", `${rand(0, 100).toFixed(4)}vw`); - el.style.setProperty("--y", `${rand(0, 100).toFixed(4)}vh`); - - if (init) { - return; - } - - const time = rand(140, 160); - - el.style.setProperty("--time", `${time.toFixed(2)}s`); - - setTimeout(() => { - place(el); - }, time * 1000); - } - - for (let i = 0; i < count; i++) { - const el = document.createElement("div"); - - el.className = "floater"; - - el.style.setProperty("--size", `${rand(2, 4, true)}px`); - el.style.setProperty("--color", colors[rand(0, colors.length, true)]); - - $floaters.appendChild(el); - - place(el, true); - - setTimeout( - () => { - place(el); - }, - rand(0, 1000, true) - ); - } -})();