From 0cc588e9cc346f0b14716739196f51633d7761da Mon Sep 17 00:00:00 2001 From: Zaine Date: Fri, 15 May 2026 12:36:03 +0100 Subject: [PATCH] memory observatory redesign x2 --- docs/hidden-memory-architecture.md | 0 src/authoring_service/constants.py | 94 +++++++-------- src/authoring_service/hidden.py | 93 +++++++++++---- src/authoring_service/templates.py | 186 ++++++++++++++++++++++------- 4 files changed, 254 insertions(+), 119 deletions(-) mode change 100644 => 100755 docs/hidden-memory-architecture.md diff --git a/docs/hidden-memory-architecture.md b/docs/hidden-memory-architecture.md old mode 100644 new mode 100755 diff --git a/src/authoring_service/constants.py b/src/authoring_service/constants.py index 11b343d..37fc3a7 100755 --- a/src/authoring_service/constants.py +++ b/src/authoring_service/constants.py @@ -3,33 +3,16 @@ from __future__ import annotations HIDDEN_CONTENT_TYPES = [ + "tooltip", "quote", + "whisper", "poem", - "hidden dialogue", - "journal entry", - "rare event", - "loading screen message", - "secret interaction", - "hidden tooltip", - "future z message", - "young z memory fragment", - "sensei chi wisdom entry", - "aphy system message", - "lima note/message", - "dream sequence", - "terminal log", - "fake error message", - "recurring joke", - "seasonal event", - "weather-based event", - "hover message", - "hidden achievement", - "guestbook entry", - "hidden conversation", - "family layer", - "search toast", - "search route", - "keyboard secret", + "observation", + "dialogue", + "secret search", + "symbolic fragment", + "ambient memory", + "hidden interaction", ] HIDDEN_CONTENT_CLASSES = [ @@ -61,33 +44,44 @@ HIDDEN_SURFACES = [ ] TYPE_ARCHITECTURE = { + "tooltip": {"contentClass": "fragment", "surfaces": ["tooltip"], "observatory": "ambient"}, "quote": {"contentClass": "fragment", "surfaces": ["quote", "observatory"], "observatory": "ambient"}, + "whisper": {"contentClass": "fragment", "surfaces": ["tooltip", "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"}, + "observation": {"contentClass": "memory", "surfaces": ["story", "observatory"], "observatory": "node"}, + "dialogue": {"contentClass": "memory", "surfaces": ["story", "observatory"], "observatory": "node"}, + "secret search": {"contentClass": "interaction", "surfaces": ["search", "hidden route"], "observatory": "event"}, + "symbolic fragment": {"contentClass": "fragment", "surfaces": ["story", "observatory"], "observatory": "ambient"}, + "ambient memory": {"contentClass": "memory", "surfaces": ["story", "observatory"], "observatory": "node"}, + "hidden interaction": {"contentClass": "interaction", "surfaces": ["hidden route", "keyboard", "play"], "observatory": "event"}, +} + +LEGACY_CONTENT_TYPE_MAP = { + "hidden tooltip": "tooltip", + "hover message": "tooltip", + "loading screen message": "whisper", + "hidden dialogue": "dialogue", + "hidden conversation": "dialogue", + "journal entry": "observation", + "future z message": "ambient memory", + "young z memory fragment": "ambient memory", + "sensei chi wisdom entry": "quote", + "aphy system message": "whisper", + "lima note/message": "whisper", + "dream sequence": "symbolic fragment", + "guestbook entry": "observation", + "terminal log": "hidden interaction", + "fake error message": "hidden interaction", + "recurring joke": "whisper", + "rare event": "hidden interaction", + "secret interaction": "hidden interaction", + "seasonal event": "hidden interaction", + "weather-based event": "hidden interaction", + "hidden achievement": "hidden interaction", + "search toast": "secret search", + "search route": "secret search", + "keyboard secret": "hidden interaction", + "family layer": "observation", } CHARACTER_REGISTRY = { diff --git a/src/authoring_service/hidden.py b/src/authoring_service/hidden.py index 49223a2..4413607 100755 --- a/src/authoring_service/hidden.py +++ b/src/authoring_service/hidden.py @@ -20,6 +20,7 @@ from .constants import ( HIDDEN_SURFACES, HIDDEN_STORY_MARKERS, HIDDEN_TONES, + LEGACY_CONTENT_TYPE_MAP, TYPE_ARCHITECTURE, ) from .utils import normalise_tags, slugify @@ -257,6 +258,7 @@ def normalize_hidden_entry(raw: dict[str, Any], existing: dict[str, Any] | None content = str(normalize_character_text_refs(entry.get("content") or "")).replace("\r\n", "\n") content_type = str(entry.get("type") or "quote").strip() content_type = str(normalize_character_text_refs(content_type)) + content_type = LEGACY_CONTENT_TYPE_MAP.get(content_type, 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"]) @@ -341,14 +343,36 @@ def normalize_hidden_story(raw: dict[str, Any], entries: list[dict[str, Any]] | if not title: raise ValueError("Every story needs a title.") ids = {entry["id"] for entry in entries or []} - raw_nodes = story.get("nodes", []) - if isinstance(raw_nodes, str): - raw_nodes = re.split(r"[,:\s]+", raw_nodes) + raw_items = story.get("items") + if not isinstance(raw_items, list): + raw_nodes = story.get("nodes", []) + if isinstance(raw_nodes, str): + raw_nodes = re.split(r"[,:\s]+", raw_nodes) + raw_items = [{"kind": "memory", "id": str(item).strip()} for item in raw_nodes if str(item).strip()] + items = [] nodes = [] - for item in raw_nodes: - node_id = str(item).strip() + for index, item in enumerate(raw_items): + if isinstance(item, str): + item = {"kind": "memory", "id": item} + if not isinstance(item, dict): + continue + kind = str(item.get("kind") or "memory").strip().lower() + if kind == "section": + section_id = slugify(str(item.get("id") or f"section-{title}-{index + 1}")) + if not section_id.startswith("section-"): + section_id = f"section-{section_id}" + items.append({ + "kind": "section", + "id": section_id, + "title": str(normalize_character_text_refs(item.get("title") or f"Section {index + 1}")).strip(), + "content": str(normalize_character_text_refs(item.get("content") or "")).replace("\r\n", "\n"), + "tone": str(item.get("tone") or story.get("tone") or "warm"), + }) + continue + node_id = str(item.get("id") or item.get("memoryId") or "").strip() if node_id and node_id not in nodes and (not ids or node_id in ids): nodes.append(node_id) + items.append({"kind": "memory", "id": node_id}) characters = normalize_character_list(story.get("characters", [])) symbols = [str(normalize_character_text_refs(item)).strip() for item in story.get("symbols", []) if str(item).strip()] markers = [slugify(str(item)) for item in story.get("markers", []) if str(item).strip()] @@ -374,6 +398,7 @@ def normalize_hidden_story(raw: dict[str, Any], entries: list[dict[str, Any]] | "tone": str(story.get("tone") or "warm"), "characters": characters, "symbols": symbols, + "items": items, "nodes": nodes, "discoveryStyle": str(story.get("discoveryStyle") or "gradual"), "layerAffinity": layer_affinity, @@ -529,11 +554,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 canonical fragments, memories, interactions, lore, 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 reusable memories, first-class stories, and story-only sections, 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 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.", + "scalability": "Stories are the primary emotional routes. Memories remain reusable artifacts, story sections hold longer one-off narrative writing, and interactions describe triggers without pretending to be narrative chapters.", } @@ -546,51 +571,71 @@ def hidden_contents(entries: list[dict[str, Any]], content_type: str) -> list[st def generated_hidden_content_block(entries: list[dict[str, Any]]) -> str: - family_layers = hidden_contents(entries, "family layer") - details = hidden_contents(entries, "hidden tooltip") + hidden_contents(entries, "hover message") + family_layers = [ + entry["content"] for entry in entries + if entry.get("enabled", True) and "Family Layer Index" in str(entry.get("category", "")) + ] + details = hidden_contents(entries, "tooltip") poems = hidden_contents(entries, "poem") - greetings = hidden_contents(entries, "loading screen message") + greetings = [ + entry["content"] for entry in hidden_entries_by_type(entries, "whisper") + if "homepage" in f"{entry.get('category', '')} {entry.get('surfaces', '')}".lower() or "loading" in entry.get("surfaces", []) + ] night_messages = [ - entry["content"] for entry in hidden_entries_by_type(entries, "rare event") + entry["content"] for entry in hidden_entries_by_type(entries, "hidden interaction") if "night" in f"{entry.get('category', '')} {entry.get('triggerConditions', '')}".lower() ] - quote_like = hidden_contents(entries, "quote") + hidden_contents(entries, "sensei chi wisdom entry") + hidden_contents(entries, "aphy system message") + hidden_contents(entries, "lima note/message") + hidden_contents(entries, "future z message") + hidden_contents(entries, "young z memory fragment") + quote_like = hidden_contents(entries, "quote") + hidden_contents(entries, "whisper") + hidden_contents(entries, "ambient memory") lore = { "quotes": quote_like, "conversations": [ entry.get("dialogue") if isinstance(entry.get("dialogue"), list) else [line for line in str(entry.get("content", "")).splitlines() if line.strip()] - for entry in hidden_entries_by_type(entries, "hidden conversation") + for entry in hidden_entries_by_type(entries, "dialogue") + ], + "journals": hidden_contents(entries, "observation"), + "warnings": [ + entry["content"] for entry in hidden_entries_by_type(entries, "hidden interaction") + if "warning" in f"{entry.get('category', '')} {entry.get('canonicalUse', '')}".lower() + ], + "dreams": hidden_contents(entries, "symbolic fragment"), + "cassettes": [ + entry["content"] for entry in hidden_entries_by_type(entries, "hidden interaction") + if "terminal" in entry.get("surfaces", []) or "cassette" in str(entry.get("category", "")).lower() + ], + "fakeUsers": [ + entry["content"] for entry in hidden_entries_by_type(entries, "observation") + if "guestbook" in f"{entry.get('category', '')} {entry.get('surfaces', '')}".lower() ], - "journals": hidden_contents(entries, "journal entry"), - "warnings": hidden_contents(entries, "fake error message"), - "dreams": hidden_contents(entries, "dream sequence"), - "cassettes": hidden_contents(entries, "terminal log"), - "fakeUsers": hidden_contents(entries, "guestbook entry"), "seasonal": { str(entry.get("season") or entry.get("triggerConditions") or entry.get("title", "")).lower().replace("season is ", ""): entry.get("content", "") - for entry in hidden_entries_by_type(entries, "seasonal event") + for entry in hidden_entries_by_type(entries, "hidden interaction") + if "seasonal" in entry.get("surfaces", []) or "season" in str(entry.get("triggerConditions", "")).lower() }, "homepageTakeovers": [ - entry["content"] for entry in hidden_entries_by_type(entries, "rare event") + entry["content"] for entry in hidden_entries_by_type(entries, "hidden interaction") if "homepage" in f"{entry.get('category', '')} {entry.get('triggerConditions', '')}".lower() ], "roomLinks": [ [entry.get("pageLocation", ""), entry.get("content", "") or entry.get("title", "")] - for entry in hidden_entries_by_type(entries, "secret interaction") + for entry in hidden_entries_by_type(entries, "hidden interaction") if entry.get("pageLocation") ], } search_toasts = { entry.get("query") or entry.get("triggerConditions") or entry.get("title", ""): entry.get("content", "") - for entry in hidden_entries_by_type(entries, "search toast") + for entry in hidden_entries_by_type(entries, "secret search") + if not entry.get("pageLocation") } search_routes = { entry.get("query") or entry.get("triggerConditions") or entry.get("title", ""): entry.get("pageLocation") or entry.get("content", "") - for entry in hidden_entries_by_type(entries, "search route") + for entry in hidden_entries_by_type(entries, "secret search") + if entry.get("pageLocation") } keyboard_secrets = [] long_keyboard_secrets = [] - for entry in hidden_entries_by_type(entries, "keyboard secret"): + for entry in hidden_entries_by_type(entries, "hidden interaction"): + if "keyboard" not in entry.get("surfaces", []) and not entry.get("keyboard"): + continue secret = dict(entry.get("keyboard") or {}) secret["phrase"] = secret.get("phrase") or entry.get("triggerConditions") or entry.get("title", "") if entry.get("pageLocation"): diff --git a/src/authoring_service/templates.py b/src/authoring_service/templates.py index 6811d5d..819dcd0 100755 --- a/src/authoring_service/templates.py +++ b/src/authoring_service/templates.py @@ -1201,7 +1201,7 @@ HIDDEN_APP_HTML = r""" // h1 { margin: 0; font-size: clamp(24px, 3vw, 42px); } h2 { margin: 0 0 10px; font-size: 18px; } h3 { margin: 0; font-size: 16px; } - .observatory { display: grid; grid-template-columns: minmax(260px, 300px) minmax(620px, 1fr) minmax(390px, 460px); height: 100vh; min-width: 0; } + .observatory { display: grid; grid-template-columns: minmax(250px, 290px) minmax(680px, 1fr) minmax(400px, 440px); height: 100vh; min-width: 0; } .left, .drawer { z-index: 3; overflow: auto; @@ -1528,6 +1528,10 @@ HIDDEN_APP_HTML = r""" border-radius: 8px; background: rgba(251, 241, 220, 0.055); } + .story-flow-item.section { + border-color: rgba(117, 169, 189, 0.3); + background: rgba(117, 169, 189, 0.09); + } .story-flow-item strong, .story-card strong { overflow-wrap: anywhere; } .story-step { width: 26px; @@ -1595,22 +1599,20 @@ HIDDEN_APP_HTML = r"""
aphy is dimming the room lights.
- - -
- + - +
- - + + +
@@ -1633,7 +1635,6 @@ HIDDEN_APP_HTML = r""" -
@@ -1695,19 +1696,27 @@ HIDDEN_APP_HTML = r""" - - + +
- +

Story flow

Drag memories here to add them to this story.
+

Memory pool

@@ -1715,8 +1724,8 @@ HIDDEN_APP_HTML = r"""

Memory editor

- - + +
@@ -1725,24 +1734,24 @@ HIDDEN_APP_HTML = r""" - - + + - + - - + - - -