/**
* The browser-local (localStorage) settings store: the defaults, load/save, and the
* `useSettings` React hook. Prompt-knob names match the engine's settings; BYOK keys
* live here too and never leave the browser except per-request.
* @module web-app/lib/settings
*/
import { useEffect, useState } from "react";
// Settings live ONLY in this browser (localStorage). No accounts, no server
// storage. BYOK API keys are part of settings and likewise never leave the
// browser except when sent per-request to the generation proxy.
//
// The prompt-knob field names match the engine's settings (settings.js) so they
// flow straight into prompt generation; image params are read by the providers.
const STORAGE_KEY = "rap.settings.v2";
/**
* @constant {object} The default SPA settings (prompt knobs match the engine; image
* params are read by the providers).
*/
export const defaultSettings = {
// Prompt
prompt: "{#random-words}",
promptCount: 1,
mode: "StableDiffusion",
// StableDiffusion | NovelAI | Midjourney
// Keyword counts
keywordCount: 5,
keywordMaxCount: 7,
// Keyword/artist source lists ("false" = fully random from the dictionaries)
keywordsFilename: "keyword",
artistFilename: "artist",
// Emphasis
keywordEmphasis: true,
emphasisChance: 0.25,
emphasisLevelChance: 0.25,
emphasisMaxLevels: 3,
deEmphasisChance: 0.25,
// Editing (add/swap/remove keywords mid-generation)
keywordEditing: true,
keywordEditingMin: 2,
keywordEditingMax: 4,
// Alternating (hybrid keywords)
keywordAlternating: true,
keywordAlternatingMaxLevels: 2,
// Artists
includeArtist: true,
minArtist: 0,
maxArtist: 2,
// Auto-append + salt
autoAddArtists: true,
autoAddFx: true,
promptSalt: false,
promptSaltStart: -1,
noAnd: false,
// List behaviour
listEntriesUsedOnce: true,
reloadListsOnPromptChange: true,
// Generation / providers
generateImages: false,
provider: "local-webui",
localWebuiUrl: "http://127.0.0.1:7860",
keys: {},
// { [providerId]: "sk-..." } — kept in this browser only
// Image params
sampler: "Euler",
imageSteps: 32,
imageWidth: 512,
imageHeight: 512,
cfg: 11,
seed: -1,
restoreFaces: false,
negativePrompt: ""
};
// The anime ("d-keyword"/"d-artist") word lists are Danbooru tag dumps that mix
// SFW and explicit adult tags with no clean separation. The Style toggle that
// selected them was removed pending a proper SFW/adult split (see
// notes/plans/removed-pending-readd.md). This migration pulls any browser that
// was left on those lists back to the safe defaults so no one is stranded on
// adult content with no UI to switch off.
function migrate(settings) {
const s = {
...settings
};
if (s.keywordsFilename === "d-keyword" || s.keywordsFilename === "d/keyword") s.keywordsFilename = "keyword";
if (s.artistFilename === "d-artist" || s.artistFilename === "d/artist") s.artistFilename = "artist";
return s;
}
/**
* @returns {object} The settings from localStorage merged over the defaults.
*/
export function loadSettings() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return {
...defaultSettings
};
return migrate({
...defaultSettings,
...JSON.parse(raw)
});
} catch {
return {
...defaultSettings
};
}
}
/**
* Persist settings to localStorage (best-effort; ignores quota / private-mode errors).
* @param {object} settings The settings to save.
* @returns {void}
*/
export function saveSettings(settings) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
} catch {
// Ignore quota / private-mode write failures — settings are best-effort.
}
}
/**
* React hook: settings state that auto-persists to localStorage on every change.
* @returns {Array} `[settings, setSettings]` — the state and its setter.
*/
// React hook: settings state that auto-persists to localStorage on change.
export function useSettings() {
const [settings, setSettings] = useState(loadSettings);
useEffect(() => saveSettings(settings), [settings]);
return [settings, setSettings];
}