diff --git a/README.md b/README.md index d9b8b0b..706c1a8 100755 --- a/README.md +++ b/README.md @@ -30,3 +30,4 @@ Code layout: - `authoring_server.py` is the compatibility executable used by systemd. - `src/authoring_service/` contains the implementation modules. - `src/tests/` contains the unit tests. +- `docs/hidden-memory-architecture.md` documents the Fragment/Memory/Story model used by the Hidden Memory Observatory. diff --git a/docs/hidden-memory-architecture.md b/docs/hidden-memory-architecture.md new file mode 100644 index 0000000..b98c606 --- /dev/null +++ b/docs/hidden-memory-architecture.md @@ -0,0 +1,82 @@ +# Hidden Memory Architecture + +The hidden ecosystem has one friendly source of truth: + +- `assets/content/hidden-details.json` in `org_web` + +The live site still consumes generated constants in: + +- `assets/scripts/hidden-details.js` in `org_web` + +## Canonical Content + +The old `type` field is now treated as the runtime delivery kind. It answers: where does this need to go in the existing JavaScript arrays? + +The newer `contentClass` field answers: what is this thing conceptually? + +Use these classes: + +- `fragment`: tiny reusable language, such as tooltips, quotes, whispers, loading lines, and small dialogue. +- `memory`: a richer emotional moment or observation that can stand as an observatory node. +- `story material`: dialogue or sequence material that is primarily useful inside a story route. +- `interaction`: a trigger, route, keyboard secret, search response, seasonal event, or play-system behavior. +- `lore`: world material such as dreams, terminal logs, guestbook entries, or hidden archive objects. +- `system layer`: structural observatory material, such as the layer guide. + +## Stories + +Stories are curated routes. They should reference entries by id through `nodes`. + +Do not duplicate paragraphs inside a story when an existing fragment or memory can be referenced. A story gives order, title, tone, unlock conditions, and emotional shape. + +## Surfaces + +The `surfaces` field says where an entry is reusable: `tooltip`, `quote`, `story`, `observatory`, `search`, `keyboard`, `play`, `dream`, `temporal`, and similar surfaces. + +A tooltip fragment can appear in the footer without becoming a full story node. A memory can appear in the observatory and in a story without being forced into a rotating quote. An interaction can trigger a hidden route without pretending to be a narrative scene. + +## Observatory Roles + +Use `observatoryRole` to keep rendering calm: + +- `ambient`: small lights and flavor, usually fragments. +- `node`: substantial emotional points, usually memories or story material. +- `event`: triggered behavior, routes, search, keyboard, seasonal, and play content. +- `guide`: layer/system material. + +The observatory should render stories as the main territories, then reveal entries inside them. Zoomed-out views should favor routes and clusters; detailed views can show individual nodes, ambient fragments, and triggered events. + +## Examples + +`lima left this page a little steadier than she found it.` + +- Class: `fragment` +- Runtime kind: `hidden tooltip` +- Surfaces: `tooltip` +- Observatory role: `ambient` + +`July 2022, on the way to uni induction, lima pops into my life` + +- Class: `memory` +- Runtime kind: `journal entry` +- Surfaces: `story`, `observatory` +- Observatory role: `node` + +`search query "lima" opens a hidden route` + +- Class: `interaction` +- Runtime kind: `search route` +- Surfaces: `search`, `hidden route` +- Observatory role: `event` + +`The story of love between two souls` + +- Class: story record, not an entry class +- References: ordered entry ids in `nodes` +- Purpose: emotional route, not duplicated content + +## Authoring Rule + +Write the smallest canonical thing that is emotionally honest. + +If it is one line, make it a fragment. If it is a moment with weight, make it a memory. If it happens because of a trigger, make it an interaction. If it is a route through existing things, make it a story. diff --git a/src/authoring_service/constants.py b/src/authoring_service/constants.py index 6ae8ee2..11b343d 100755 --- a/src/authoring_service/constants.py +++ b/src/authoring_service/constants.py @@ -32,6 +32,64 @@ HIDDEN_CONTENT_TYPES = [ "keyboard secret", ] +HIDDEN_CONTENT_CLASSES = [ + "fragment", + "memory", + "story material", + "interaction", + "lore", + "system layer", +] + +HIDDEN_SURFACES = [ + "tooltip", + "quote", + "poem", + "story", + "observatory", + "hidden route", + "search", + "keyboard", + "play", + "dream", + "temporal", + "seasonal", + "loading", + "guestbook", + "terminal", + "layer guide", +] + +TYPE_ARCHITECTURE = { + "quote": {"contentClass": "fragment", "surfaces": ["quote", "observatory"], "observatory": "ambient"}, + "poem": {"contentClass": "fragment", "surfaces": ["poem", "observatory"], "observatory": "ambient"}, + "hidden dialogue": {"contentClass": "fragment", "surfaces": ["tooltip", "story", "observatory"], "observatory": "node"}, + "journal entry": {"contentClass": "memory", "surfaces": ["story", "observatory"], "observatory": "node"}, + "rare event": {"contentClass": "interaction", "surfaces": ["hidden route", "play", "observatory"], "observatory": "event"}, + "loading screen message": {"contentClass": "fragment", "surfaces": ["loading", "quote"], "observatory": "ambient"}, + "secret interaction": {"contentClass": "interaction", "surfaces": ["hidden route", "play"], "observatory": "event"}, + "hidden tooltip": {"contentClass": "fragment", "surfaces": ["tooltip"], "observatory": "ambient"}, + "future z message": {"contentClass": "memory", "surfaces": ["temporal", "story", "observatory"], "observatory": "node"}, + "young z memory fragment": {"contentClass": "memory", "surfaces": ["story", "observatory"], "observatory": "node"}, + "sensei chi wisdom entry": {"contentClass": "fragment", "surfaces": ["quote", "story", "observatory"], "observatory": "ambient"}, + "aphy system message": {"contentClass": "fragment", "surfaces": ["quote", "terminal"], "observatory": "ambient"}, + "lima note/message": {"contentClass": "fragment", "surfaces": ["quote", "story"], "observatory": "ambient"}, + "dream sequence": {"contentClass": "lore", "surfaces": ["dream", "play", "story", "observatory"], "observatory": "node"}, + "terminal log": {"contentClass": "lore", "surfaces": ["terminal", "play"], "observatory": "event"}, + "fake error message": {"contentClass": "interaction", "surfaces": ["play", "hidden route"], "observatory": "event"}, + "recurring joke": {"contentClass": "fragment", "surfaces": ["tooltip", "quote"], "observatory": "ambient"}, + "seasonal event": {"contentClass": "interaction", "surfaces": ["seasonal", "hidden route"], "observatory": "event"}, + "weather-based event": {"contentClass": "interaction", "surfaces": ["hidden route"], "observatory": "event"}, + "hover message": {"contentClass": "fragment", "surfaces": ["tooltip"], "observatory": "ambient"}, + "hidden achievement": {"contentClass": "interaction", "surfaces": ["play", "hidden route"], "observatory": "event"}, + "guestbook entry": {"contentClass": "lore", "surfaces": ["guestbook", "observatory"], "observatory": "ambient"}, + "hidden conversation": {"contentClass": "story material", "surfaces": ["story", "observatory"], "observatory": "node"}, + "family layer": {"contentClass": "system layer", "surfaces": ["layer guide"], "observatory": "guide"}, + "search toast": {"contentClass": "interaction", "surfaces": ["search"], "observatory": "event"}, + "search route": {"contentClass": "interaction", "surfaces": ["search", "hidden route"], "observatory": "event"}, + "keyboard secret": {"contentClass": "interaction", "surfaces": ["keyboard", "hidden route"], "observatory": "event"}, +} + CHARACTER_REGISTRY = { "young z": { "id": "young z", diff --git a/src/authoring_service/hidden.py b/src/authoring_service/hidden.py index 8ee4d83..49223a2 100755 --- a/src/authoring_service/hidden.py +++ b/src/authoring_service/hidden.py @@ -12,12 +12,15 @@ from .config import HIDDEN_BACKUP_DIR, HIDDEN_CONTENT_JSON, HIDDEN_DETAILS_JS, R from .constants import ( CHARACTER_REGISTRY, HIDDEN_CHARACTERS, + HIDDEN_CONTENT_CLASSES, HIDDEN_CONTENT_TYPES, HIDDEN_DISCOVERY_STYLES, HIDDEN_LAYER_DEPTHS, HIDDEN_RARITIES, + HIDDEN_SURFACES, HIDDEN_STORY_MARKERS, HIDDEN_TONES, + TYPE_ARCHITECTURE, ) from .utils import normalise_tags, slugify @@ -147,9 +150,11 @@ def make_hidden_entry(content_type: str, content: str, index: int, **extra: Any) title = str(extra.pop("title", "") or hidden_title(content_type, content, index)) characters = extra.pop("characters", None) or detect_hidden_characters(f"{title} {content}") tags = extra.pop("tags", None) or [slugify(character) for character in characters] + architecture = TYPE_ARCHITECTURE.get(content_type, TYPE_ARCHITECTURE["quote"]) entry = { "id": hidden_entry_id(content_type, index, title), "type": content_type, + "contentClass": extra.pop("contentClass", architecture["contentClass"]), "title": title, "content": content, "characters": characters, @@ -182,6 +187,9 @@ def make_hidden_entry(content_type: str, content: str, index: int, **extra: Any) "thematicLinks": extra.pop("thematicLinks", []), "symbolicLinks": extra.pop("symbolicLinks", []), "triggerLinks": extra.pop("triggerLinks", []), + "surfaces": extra.pop("surfaces", architecture["surfaces"]), + "observatoryRole": extra.pop("observatoryRole", architecture["observatory"]), + "canonicalUse": extra.pop("canonicalUse", ""), } entry.update(extra) return entry @@ -251,6 +259,7 @@ def normalize_hidden_entry(raw: dict[str, Any], existing: dict[str, Any] | None content_type = str(normalize_character_text_refs(content_type)) if content_type not in HIDDEN_CONTENT_TYPES: raise ValueError(f"Unsupported hidden content type: {content_type}") + architecture = TYPE_ARCHITECTURE.get(content_type, TYPE_ARCHITECTURE["quote"]) if not title: raise ValueError("Every hidden entry needs a title.") if not content and content_type not in {"search route"}: @@ -258,6 +267,10 @@ def normalize_hidden_entry(raw: dict[str, Any], existing: dict[str, Any] | None entry["id"] = slugify(str(entry.get("id") or title)) entry["title"] = title entry["type"] = content_type + content_class = str(entry.get("contentClass") or architecture["contentClass"]).strip() + if content_class not in HIDDEN_CONTENT_CLASSES: + content_class = architecture["contentClass"] + entry["contentClass"] = content_class entry["content"] = content detected_characters = detect_hidden_characters(" ".join([ title, @@ -305,8 +318,15 @@ def normalize_hidden_entry(raw: dict[str, Any], existing: dict[str, Any] | None "thematicLinks", "symbolicLinks", "triggerLinks", + "surfaces", ]: entry[key] = [str(normalize_character_text_refs(item)).strip() for item in entry.get(key, []) if str(item).strip()] + if not entry["surfaces"]: + entry["surfaces"] = list(architecture["surfaces"]) + entry["surfaces"] = [surface for surface in entry["surfaces"] if surface in HIDDEN_SURFACES] or list(architecture["surfaces"]) + observatory_role = str(entry.get("observatoryRole") or architecture["observatory"]).strip() + entry["observatoryRole"] = observatory_role if observatory_role in {"ambient", "node", "event", "guide"} else architecture["observatory"] + entry["canonicalUse"] = str(normalize_character_text_refs(entry.get("canonicalUse") or "")) for key in ["dialogue", "keyboard"]: if key in entry: entry[key] = normalize_character_text_refs(entry[key]) @@ -444,12 +464,15 @@ def load_hidden_store() -> dict[str, Any]: normalized_stories = migrate_hidden_stories(normalized) migrated_relationships = bool(normalized_stories) return { - "schemaVersion": 2, + "schemaVersion": 3, "source": HIDDEN_DETAILS_JS.relative_to(ROOT).as_posix(), "contentPath": HIDDEN_CONTENT_JSON.relative_to(ROOT).as_posix(), "migratedFromJs": migrated, "migratedRelationshipsToStories": migrated_relationships, "types": HIDDEN_CONTENT_TYPES, + "contentClasses": HIDDEN_CONTENT_CLASSES, + "surfaces": HIDDEN_SURFACES, + "typeArchitecture": TYPE_ARCHITECTURE, "characters": HIDDEN_CHARACTERS, "characterRegistry": CHARACTER_REGISTRY, "validation": validate_hidden_integrity(normalized, normalized_stories), @@ -506,11 +529,11 @@ def validate_hidden_integrity(entries: list[dict[str, Any]], stories: list[dict[ def hidden_architecture_recommendations() -> dict[str, Any]: return { - "storage": "Use assets/content/hidden-details.json as the friendly source of truth for memories and first-class stories, then regenerate the editable constants in assets/scripts/hidden-details.js.", + "storage": "Use assets/content/hidden-details.json as the friendly source of truth for canonical fragments, memories, interactions, lore, and first-class stories, then regenerate the editable constants in assets/scripts/hidden-details.js.", "backups": "Every save writes timestamped backups for both JSON and JS under backups/hidden-details/.", "versioning": "Commit the JSON and generated JS together so the live site and authoring history stay aligned.", - "collaboration": "For simultaneous editing, resolve conflicts by memory id and story id rather than by whole-file ownership.", - "scalability": "Stories are the primary emotional routes. Legacy relationship fields remain readable for migration, but the observatory should stay story-first.", + "collaboration": "For simultaneous editing, resolve conflicts by entry id and story id rather than by whole-file ownership.", + "scalability": "Stories are the primary emotional routes. Fragments remain reusable ambient language, memories become observatory nodes, and interactions describe triggers without pretending to be narrative scenes.", } @@ -640,7 +663,7 @@ def save_hidden_store(payload: dict[str, Any]) -> dict[str, Any]: HIDDEN_CONTENT_JSON.write_text( json.dumps( { - "schemaVersion": 2, + "schemaVersion": 3, "generatedAt": datetime.now().isoformat(timespec="seconds"), "entries": entries, "stories": stories, @@ -656,5 +679,3 @@ def save_hidden_store(payload: dict[str, Any]) -> dict[str, Any]: saved["message"] = "stored safely. future-you will probably smile at this one." saved["backupStamp"] = stamp return saved - - diff --git a/src/authoring_service/templates.py b/src/authoring_service/templates.py index 523f749..6811d5d 100755 --- a/src/authoring_service/templates.py +++ b/src/authoring_service/templates.py @@ -1609,7 +1609,7 @@ HIDDEN_APP_HTML = r"""
- +
@@ -1715,7 +1715,8 @@ HIDDEN_APP_HTML = r"""

Memory editor

- + +
@@ -1727,7 +1728,10 @@ HIDDEN_APP_HTML = r""" - + + + + @@ -1757,7 +1761,8 @@ HIDDEN_APP_HTML = r"""

Architecture

-

Story model: stories are first-class emotional routes. Memories can belong to multiple stories, but routes stay readable and authored in order.

+

Canonical model: fragments are tiny reusable lines, memories are richer observatory moments, interactions are triggers, lore is world material, and stories are curated routes through them.

+

Story model: stories are first-class emotional routes. Entries can belong to multiple stories, but routes stay readable and authored in order.

Legacy links: old continuations, echoes, and symbolic links are retained for migration only. New authoring happens through Story Builder.

Storage: the friendly graph lives in assets/content/hidden-details.json; the live site still receives generated constants in assets/scripts/hidden-details.js.

@@ -1771,7 +1776,7 @@ HIDDEN_APP_HTML = r""" const WORLD = { width: 4600, height: 3300, cx: 2300, cy: 1650 }; const SCREEN = { width: 1000, height: 760 }; const state = { entries: [], stories: [], meta: {}, mode: "graph", selectedId: "", selectedStoryId: "", undo: [], redo: [], draggingId: "", positions: new Map(), clusterPositions: new Map(), zoom: 0, focusCluster: "", focusKind: "", focusIds: null, focusLabel: "", history: [], explorationUndo: [], explorationRedo: [], camera: { x: 0, y: 0, k: 0.22 }, pan: null, drag: null, renderTimer: 0 }; - const fields = ["title","type","familyLayer","characters","tone","rarity","discoveryDifficulty","mysteryLevel","resonanceScore","pageLocation","triggerConditions","content","symbols","narrativeArcs","tags","emotionalRole","continuationLinks","echoes","thematicLinks","symbolicLinks","triggerLinks","parentLinks","childLinks","mirroredEntries","enabled","cssClassHooks","audioSettings","animationTrigger","notes"]; + const fields = ["title","type","contentClass","familyLayer","characters","tone","rarity","discoveryDifficulty","mysteryLevel","resonanceScore","pageLocation","triggerConditions","content","surfaces","observatoryRole","canonicalUse","symbols","narrativeArcs","tags","emotionalRole","continuationLinks","echoes","thematicLinks","symbolicLinks","triggerLinks","parentLinks","childLinks","mirroredEntries","enabled","cssClassHooks","audioSettings","animationTrigger","notes"]; const storyFields = ["storyTitle","storyDescription","storyTone","storyDiscovery","storyCharacters","storySymbols","storyMarkers","storyLayers","storyUnlock","storyHidden"]; const $ = (id) => document.getElementById(id); const toneColors = { warm: "#d3a64d", funny: "#7fb089", nostalgic: "#d06b78", wise: "#75a9bd", strange: "#a58ac9", soft: "#e6bd8c", hopeful: "#9fcf9f", protective: "#d98f71", melancholy: "#8fa7c6" }; @@ -1838,11 +1843,14 @@ HIDDEN_APP_HTML = r""" entry.title, entry.content, entry.type, + entry.contentClass, entry.triggerConditions, entry.pageLocation, + entry.canonicalUse, entry.emotionalRole, entry.mysteryLevel, ...(entry.tags || []), + ...(entry.surfaces || []), ...(entry.symbols || []), ...(entry.narrativeArcs || []), ].join(" ").toLowerCase(); @@ -2065,8 +2073,9 @@ HIDDEN_APP_HTML = r""" return { ...current, id: current.id || slugify($("title").value), - title: $("title").value.trim() || "Untitled memory", + title: $("title").value.trim() || "Untitled fragment", type: $("type").value, + contentClass: $("contentClass").value, familyLayer: $("familyLayer").value, characters: normalizeCharacters($("characters").value), emotionalTone: $("tone").value, @@ -2077,6 +2086,9 @@ HIDDEN_APP_HTML = r""" pageLocation: $("pageLocation").value, triggerConditions: $("triggerConditions").value, content: $("content").value, + surfaces: splitList($("surfaces").value), + observatoryRole: $("observatoryRole").value, + canonicalUse: $("canonicalUse").value, symbols: splitList($("symbols").value), narrativeArcs: splitList($("narrativeArcs").value), tags: splitList($("tags").value), @@ -2096,6 +2108,12 @@ HIDDEN_APP_HTML = r""" notes: $("notes").value, }; } + function applyTypeDefaults() { + const architecture = state.meta.typeArchitecture?.[$("type").value] || {}; + if (architecture.contentClass) $("contentClass").value = architecture.contentClass; + if (architecture.surfaces) $("surfaces").value = architecture.surfaces.join(", "); + if (architecture.observatory) $("observatoryRole").value = architecture.observatory; + } function applyFormToState() { if (!state.selectedId) return; const index = state.entries.findIndex((entry) => entry.id === state.selectedId); @@ -2158,9 +2176,10 @@ HIDDEN_APP_HTML = r""" function fillForm(entry) { if (!entry) return; $("drawerTitle").textContent = entry.title || "Untitled memory"; - $("drawerSub").textContent = `${layerName(depth(entry))} / ${entry.type || "memory"}`; + $("drawerSub").textContent = `${layerName(depth(entry))} / ${entry.contentClass || "fragment"} / ${entry.type || "entry"}`; $("title").value = entry.title || ""; $("type").value = entry.type || "quote"; + $("contentClass").value = entry.contentClass || state.meta.typeArchitecture?.[entry.type]?.contentClass || "fragment"; $("familyLayer").value = String(depth(entry)); $("characters").value = (entry.characters || []).join(", "); $("tone").value = entry.emotionalTone || "warm"; @@ -2171,6 +2190,9 @@ HIDDEN_APP_HTML = r""" $("pageLocation").value = entry.pageLocation || ""; $("triggerConditions").value = entry.triggerConditions || ""; $("content").value = entry.content || ""; + $("surfaces").value = (entry.surfaces || []).join(", "); + $("observatoryRole").value = entry.observatoryRole || state.meta.typeArchitecture?.[entry.type]?.observatory || "ambient"; + $("canonicalUse").value = entry.canonicalUse || ""; $("symbols").value = (entry.symbols || []).join(", "); $("narrativeArcs").value = (entry.narrativeArcs || []).join(", "); $("tags").value = (entry.tags || []).join(", "); @@ -2206,7 +2228,7 @@ HIDDEN_APP_HTML = r""" function filteredEntries() { const query = $("search").value.toLowerCase(); return state.entries.filter((entry) => { - const haystack = [entry.title, entry.content, entry.type, entry.triggerConditions, entry.pageLocation, entry.emotionalRole, entry.mysteryLevel, ...(entry.characters || []), ...(entry.tags || []), ...(entry.symbols || []), ...(entry.narrativeArcs || [])].join(" ").toLowerCase(); + const haystack = [entry.title, entry.content, entry.type, entry.contentClass, entry.triggerConditions, entry.pageLocation, entry.canonicalUse, entry.emotionalRole, entry.mysteryLevel, ...(entry.surfaces || []), ...(entry.characters || []), ...(entry.tags || []), ...(entry.symbols || []), ...(entry.narrativeArcs || [])].join(" ").toLowerCase(); return (!query || haystack.includes(query)) && (!$("layerFilter").value || String(depth(entry)) === $("layerFilter").value) && (!$("characterFilter").value || (entry.characters || []).includes($("characterFilter").value)) @@ -2619,7 +2641,7 @@ HIDDEN_APP_HTML = r""" const scale = labelScale(); const deep = state.camera.k > 1.25 || selected; const text = truncate(entry.title, deep ? 72 : state.camera.k > 0.85 ? 42 : 24); - const meta = `${entry.type || "memory"} / ${entry.emotionalTone || "warm"}`; + const meta = `${entry.contentClass || "fragment"} / ${entry.type || "entry"} / ${entry.emotionalTone || "warm"}`; const width = Math.max(190, Math.min(420, text.length * 8 + 42)); const height = deep ? 92 : 44; const labelPos = { x: pos.x + size + 18, y: pos.y - height * scale / 2 }; @@ -3135,7 +3157,7 @@ HIDDEN_APP_HTML = r""" function renderPreview(entry) { const body = html(entry.content || "").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1"); const avatarPills = charactersForEntry(entry).map((character) => `${avatarImg(character, "tiny", avatarExpressionForEntry(entry))}${html(characterLabel(character))}`).join(""); - $("preview").innerHTML = `${body || "This memory is waiting for words."}
${avatarPills}${html(layerName(depth(entry)))}${html(entry.emotionalTone)}${html(entry.rarity)}${(entry.symbols || []).map((item) => `${html(item)}`).join("")}
`; + $("preview").innerHTML = `${body || "This entry is waiting for words."}
${avatarPills}${html(entry.contentClass || "fragment")}${html(entry.observatoryRole || "ambient")}${html(layerName(depth(entry)))}${html(entry.emotionalTone)}${html(entry.rarity)}${(entry.surfaces || []).map((item) => `${html(item)}`).join("")}${(entry.symbols || []).map((item) => `${html(item)}`).join("")}
`; } function renderConnections(entry) { const stories = storiesForEntry(entry.id); @@ -3201,13 +3223,17 @@ HIDDEN_APP_HTML = r""" const now = new Date().toISOString().slice(0, 10); const entry = { id: `memory-${Date.now()}`, - title: "Untitled memory", + title: "Untitled fragment", type, + contentClass: state.meta.typeArchitecture?.[type]?.contentClass || "fragment", content: "", characters: [], emotionalTone: "warm", rarity: "common", triggerConditions: "", + surfaces: state.meta.typeArchitecture?.[type]?.surfaces || ["quote"], + observatoryRole: state.meta.typeArchitecture?.[type]?.observatory || "ambient", + canonicalUse: "", tags: [], category: type, pageLocation: "", @@ -3340,6 +3366,7 @@ HIDDEN_APP_HTML = r""" function populateControls() { const opt = (value, label = value) => ``; $("type").innerHTML = state.meta.types.map((value) => opt(value)).join(""); + $("contentClass").innerHTML = (state.meta.contentClasses || ["fragment","memory"]).map((value) => opt(value)).join(""); $("tone").innerHTML = state.meta.tones.map((value) => opt(value)).join(""); $("storyTone").innerHTML = state.meta.tones.map((value) => opt(value)).join(""); $("storyDiscovery").innerHTML = (state.meta.discoveryStyles || ["gradual"]).map((value) => opt(value)).join(""); @@ -3491,6 +3518,10 @@ HIDDEN_APP_HTML = r""" $(id).addEventListener("input", applyFormToState); $(id).addEventListener("change", () => { snapshot(); applyFormToState(); }); }); + $("type").addEventListener("change", () => { + applyTypeDefaults(); + applyFormToState(); + }); storyFields.forEach((id) => { $(id).addEventListener("input", applyStoryFormToState); $(id).addEventListener("change", () => { snapshot(); applyStoryFormToState(); });