memory observatory redesign x2
All checks were successful
Build Authoring Service / build (push) Successful in 9s
All checks were successful
Build Authoring Service / build (push) Successful in 9s
This commit is contained in:
0
docs/hidden-memory-architecture.md
Normal file → Executable file
0
docs/hidden-memory-architecture.md
Normal file → Executable file
@@ -3,33 +3,16 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
HIDDEN_CONTENT_TYPES = [
|
HIDDEN_CONTENT_TYPES = [
|
||||||
|
"tooltip",
|
||||||
"quote",
|
"quote",
|
||||||
|
"whisper",
|
||||||
"poem",
|
"poem",
|
||||||
"hidden dialogue",
|
"observation",
|
||||||
"journal entry",
|
"dialogue",
|
||||||
"rare event",
|
"secret search",
|
||||||
"loading screen message",
|
"symbolic fragment",
|
||||||
"secret interaction",
|
"ambient memory",
|
||||||
"hidden tooltip",
|
"hidden interaction",
|
||||||
"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",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
HIDDEN_CONTENT_CLASSES = [
|
HIDDEN_CONTENT_CLASSES = [
|
||||||
@@ -61,33 +44,44 @@ HIDDEN_SURFACES = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
TYPE_ARCHITECTURE = {
|
TYPE_ARCHITECTURE = {
|
||||||
|
"tooltip": {"contentClass": "fragment", "surfaces": ["tooltip"], "observatory": "ambient"},
|
||||||
"quote": {"contentClass": "fragment", "surfaces": ["quote", "observatory"], "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"},
|
"poem": {"contentClass": "fragment", "surfaces": ["poem", "observatory"], "observatory": "ambient"},
|
||||||
"hidden dialogue": {"contentClass": "fragment", "surfaces": ["tooltip", "story", "observatory"], "observatory": "node"},
|
"observation": {"contentClass": "memory", "surfaces": ["story", "observatory"], "observatory": "node"},
|
||||||
"journal entry": {"contentClass": "memory", "surfaces": ["story", "observatory"], "observatory": "node"},
|
"dialogue": {"contentClass": "memory", "surfaces": ["story", "observatory"], "observatory": "node"},
|
||||||
"rare event": {"contentClass": "interaction", "surfaces": ["hidden route", "play", "observatory"], "observatory": "event"},
|
"secret search": {"contentClass": "interaction", "surfaces": ["search", "hidden route"], "observatory": "event"},
|
||||||
"loading screen message": {"contentClass": "fragment", "surfaces": ["loading", "quote"], "observatory": "ambient"},
|
"symbolic fragment": {"contentClass": "fragment", "surfaces": ["story", "observatory"], "observatory": "ambient"},
|
||||||
"secret interaction": {"contentClass": "interaction", "surfaces": ["hidden route", "play"], "observatory": "event"},
|
"ambient memory": {"contentClass": "memory", "surfaces": ["story", "observatory"], "observatory": "node"},
|
||||||
"hidden tooltip": {"contentClass": "fragment", "surfaces": ["tooltip"], "observatory": "ambient"},
|
"hidden interaction": {"contentClass": "interaction", "surfaces": ["hidden route", "keyboard", "play"], "observatory": "event"},
|
||||||
"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"},
|
LEGACY_CONTENT_TYPE_MAP = {
|
||||||
"aphy system message": {"contentClass": "fragment", "surfaces": ["quote", "terminal"], "observatory": "ambient"},
|
"hidden tooltip": "tooltip",
|
||||||
"lima note/message": {"contentClass": "fragment", "surfaces": ["quote", "story"], "observatory": "ambient"},
|
"hover message": "tooltip",
|
||||||
"dream sequence": {"contentClass": "lore", "surfaces": ["dream", "play", "story", "observatory"], "observatory": "node"},
|
"loading screen message": "whisper",
|
||||||
"terminal log": {"contentClass": "lore", "surfaces": ["terminal", "play"], "observatory": "event"},
|
"hidden dialogue": "dialogue",
|
||||||
"fake error message": {"contentClass": "interaction", "surfaces": ["play", "hidden route"], "observatory": "event"},
|
"hidden conversation": "dialogue",
|
||||||
"recurring joke": {"contentClass": "fragment", "surfaces": ["tooltip", "quote"], "observatory": "ambient"},
|
"journal entry": "observation",
|
||||||
"seasonal event": {"contentClass": "interaction", "surfaces": ["seasonal", "hidden route"], "observatory": "event"},
|
"future z message": "ambient memory",
|
||||||
"weather-based event": {"contentClass": "interaction", "surfaces": ["hidden route"], "observatory": "event"},
|
"young z memory fragment": "ambient memory",
|
||||||
"hover message": {"contentClass": "fragment", "surfaces": ["tooltip"], "observatory": "ambient"},
|
"sensei chi wisdom entry": "quote",
|
||||||
"hidden achievement": {"contentClass": "interaction", "surfaces": ["play", "hidden route"], "observatory": "event"},
|
"aphy system message": "whisper",
|
||||||
"guestbook entry": {"contentClass": "lore", "surfaces": ["guestbook", "observatory"], "observatory": "ambient"},
|
"lima note/message": "whisper",
|
||||||
"hidden conversation": {"contentClass": "story material", "surfaces": ["story", "observatory"], "observatory": "node"},
|
"dream sequence": "symbolic fragment",
|
||||||
"family layer": {"contentClass": "system layer", "surfaces": ["layer guide"], "observatory": "guide"},
|
"guestbook entry": "observation",
|
||||||
"search toast": {"contentClass": "interaction", "surfaces": ["search"], "observatory": "event"},
|
"terminal log": "hidden interaction",
|
||||||
"search route": {"contentClass": "interaction", "surfaces": ["search", "hidden route"], "observatory": "event"},
|
"fake error message": "hidden interaction",
|
||||||
"keyboard secret": {"contentClass": "interaction", "surfaces": ["keyboard", "hidden route"], "observatory": "event"},
|
"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 = {
|
CHARACTER_REGISTRY = {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from .constants import (
|
|||||||
HIDDEN_SURFACES,
|
HIDDEN_SURFACES,
|
||||||
HIDDEN_STORY_MARKERS,
|
HIDDEN_STORY_MARKERS,
|
||||||
HIDDEN_TONES,
|
HIDDEN_TONES,
|
||||||
|
LEGACY_CONTENT_TYPE_MAP,
|
||||||
TYPE_ARCHITECTURE,
|
TYPE_ARCHITECTURE,
|
||||||
)
|
)
|
||||||
from .utils import normalise_tags, slugify
|
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 = 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(entry.get("type") or "quote").strip()
|
||||||
content_type = str(normalize_character_text_refs(content_type))
|
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:
|
if content_type not in HIDDEN_CONTENT_TYPES:
|
||||||
raise ValueError(f"Unsupported hidden content type: {content_type}")
|
raise ValueError(f"Unsupported hidden content type: {content_type}")
|
||||||
architecture = TYPE_ARCHITECTURE.get(content_type, TYPE_ARCHITECTURE["quote"])
|
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:
|
if not title:
|
||||||
raise ValueError("Every story needs a title.")
|
raise ValueError("Every story needs a title.")
|
||||||
ids = {entry["id"] for entry in entries or []}
|
ids = {entry["id"] for entry in entries or []}
|
||||||
raw_nodes = story.get("nodes", [])
|
raw_items = story.get("items")
|
||||||
if isinstance(raw_nodes, str):
|
if not isinstance(raw_items, list):
|
||||||
raw_nodes = re.split(r"[,:\s]+", raw_nodes)
|
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 = []
|
nodes = []
|
||||||
for item in raw_nodes:
|
for index, item in enumerate(raw_items):
|
||||||
node_id = str(item).strip()
|
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):
|
if node_id and node_id not in nodes and (not ids or node_id in ids):
|
||||||
nodes.append(node_id)
|
nodes.append(node_id)
|
||||||
|
items.append({"kind": "memory", "id": node_id})
|
||||||
characters = normalize_character_list(story.get("characters", []))
|
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()]
|
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()]
|
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"),
|
"tone": str(story.get("tone") or "warm"),
|
||||||
"characters": characters,
|
"characters": characters,
|
||||||
"symbols": symbols,
|
"symbols": symbols,
|
||||||
|
"items": items,
|
||||||
"nodes": nodes,
|
"nodes": nodes,
|
||||||
"discoveryStyle": str(story.get("discoveryStyle") or "gradual"),
|
"discoveryStyle": str(story.get("discoveryStyle") or "gradual"),
|
||||||
"layerAffinity": layer_affinity,
|
"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]:
|
def hidden_architecture_recommendations() -> dict[str, Any]:
|
||||||
return {
|
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/.",
|
"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.",
|
"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.",
|
"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:
|
def generated_hidden_content_block(entries: list[dict[str, Any]]) -> str:
|
||||||
family_layers = hidden_contents(entries, "family layer")
|
family_layers = [
|
||||||
details = hidden_contents(entries, "hidden tooltip") + hidden_contents(entries, "hover message")
|
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")
|
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 = [
|
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()
|
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 = {
|
lore = {
|
||||||
"quotes": quote_like,
|
"quotes": quote_like,
|
||||||
"conversations": [
|
"conversations": [
|
||||||
entry.get("dialogue") if isinstance(entry.get("dialogue"), list) else [line for line in str(entry.get("content", "")).splitlines() if line.strip()]
|
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": {
|
"seasonal": {
|
||||||
str(entry.get("season") or entry.get("triggerConditions") or entry.get("title", "")).lower().replace("season is ", ""): entry.get("content", "")
|
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": [
|
"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()
|
if "homepage" in f"{entry.get('category', '')} {entry.get('triggerConditions', '')}".lower()
|
||||||
],
|
],
|
||||||
"roomLinks": [
|
"roomLinks": [
|
||||||
[entry.get("pageLocation", ""), entry.get("content", "") or entry.get("title", "")]
|
[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")
|
if entry.get("pageLocation")
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
search_toasts = {
|
search_toasts = {
|
||||||
entry.get("query") or entry.get("triggerConditions") or entry.get("title", ""): entry.get("content", "")
|
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 = {
|
search_routes = {
|
||||||
entry.get("query") or entry.get("triggerConditions") or entry.get("title", ""): entry.get("pageLocation") or entry.get("content", "")
|
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 = []
|
keyboard_secrets = []
|
||||||
long_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 = dict(entry.get("keyboard") or {})
|
||||||
secret["phrase"] = secret.get("phrase") or entry.get("triggerConditions") or entry.get("title", "")
|
secret["phrase"] = secret.get("phrase") or entry.get("triggerConditions") or entry.get("title", "")
|
||||||
if entry.get("pageLocation"):
|
if entry.get("pageLocation"):
|
||||||
|
|||||||
@@ -1201,7 +1201,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
// h1 { margin: 0; font-size: clamp(24px, 3vw, 42px); }
|
// h1 { margin: 0; font-size: clamp(24px, 3vw, 42px); }
|
||||||
h2 { margin: 0 0 10px; font-size: 18px; }
|
h2 { margin: 0 0 10px; font-size: 18px; }
|
||||||
h3 { margin: 0; font-size: 16px; }
|
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 {
|
.left, .drawer {
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@@ -1528,6 +1528,10 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: rgba(251, 241, 220, 0.055);
|
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-flow-item strong, .story-card strong { overflow-wrap: anywhere; }
|
||||||
.story-step {
|
.story-step {
|
||||||
width: 26px;
|
width: 26px;
|
||||||
@@ -1595,22 +1599,20 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
<div id="status" class="status">aphy is dimming the room lights.</div>
|
<div id="status" class="status">aphy is dimming the room lights.</div>
|
||||||
<div class="modebar" id="modebar">
|
<div class="modebar" id="modebar">
|
||||||
<button data-mode="graph" class="active">Story constellations</button>
|
<button data-mode="graph" class="active">Story constellations</button>
|
||||||
<button data-mode="journey">Emotional journeys</button>
|
|
||||||
<button data-mode="hidden">Hidden routes</button>
|
<button data-mode="hidden">Hidden routes</button>
|
||||||
<button data-mode="character">Character stories</button>
|
<button data-mode="character">Character stories</button>
|
||||||
<button data-mode="dream">Dream paths</button>
|
|
||||||
<button data-mode="temporal">Temporal stories</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="filters">
|
<div class="filters">
|
||||||
<input id="search" type="search" placeholder="Search memories, symbols, triggers" />
|
<input id="search" type="search" placeholder="Search memories and stories" />
|
||||||
<select id="layerFilter"></select>
|
<select id="layerFilter"></select>
|
||||||
<select id="characterFilter"></select>
|
<select id="characterFilter"></select>
|
||||||
<select id="toneFilter"></select>
|
<select id="toneFilter"></select>
|
||||||
<select id="rarityFilter"></select>
|
<select id="rarityFilter" class="hidden"></select>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button id="newBtn" type="button">New fragment</button>
|
<button id="newBtn" type="button">New memory</button>
|
||||||
<button id="saveBtn" class="primary" type="button">Save constellation</button>
|
<button id="newStoryBtn" type="button">New story</button>
|
||||||
|
<button id="saveBtn" class="primary" type="button">Save</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions" style="margin-top:8px">
|
<div class="actions" style="margin-top:8px">
|
||||||
<button id="softResetBtn" type="button">Clear Focus</button>
|
<button id="softResetBtn" type="button">Clear Focus</button>
|
||||||
@@ -1633,7 +1635,6 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
<button data-tour="rare" type="button">show the rarest memories</button>
|
<button data-tour="rare" type="button">show the rarest memories</button>
|
||||||
<button data-tour="future" type="button">show discoveries tied to future z</button>
|
<button data-tour="future" type="button">show discoveries tied to future z</button>
|
||||||
<button data-tour="hidden">show hidden routes</button>
|
<button data-tour="hidden">show hidden routes</button>
|
||||||
<button data-tour="dream">show dream paths</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="guide-card">
|
<div class="guide-card">
|
||||||
@@ -1695,19 +1696,27 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
<label>Discovery<select id="storyDiscovery"></select></label>
|
<label>Discovery<select id="storyDiscovery"></select></label>
|
||||||
<label>Characters<input id="storyCharacters" placeholder="lima, z" /></label>
|
<label>Characters<input id="storyCharacters" placeholder="lima, z" /></label>
|
||||||
<label>Symbols<input id="storySymbols" placeholder="ring, kitchen light" /></label>
|
<label>Symbols<input id="storySymbols" placeholder="ring, kitchen light" /></label>
|
||||||
<label>Markers<select id="storyMarkers" multiple size="4"></select></label>
|
<label>Markers<select id="storyMarkers" multiple size="3"></select></label>
|
||||||
<label>Layers<input id="storyLayers" placeholder="1, 2, 3" /></label>
|
<label>Depth<input id="storyLayers" placeholder="1, 2, 3" /></label>
|
||||||
<label class="full">Hidden unlock conditions<input id="storyUnlock" placeholder="find kitchen light, return at night" /></label>
|
<label class="full">Hidden unlock conditions<input id="storyUnlock" placeholder="find kitchen light, return at night" /></label>
|
||||||
<label>Visibility<select id="storyHidden"><option value="false">Public</option><option value="true">Hidden</option></select></label>
|
<label>Visibility<select id="storyHidden"><option value="false">Public</option><option value="true">Hidden</option></select></label>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions" style="margin-top:10px">
|
<div class="actions" style="margin-top:10px">
|
||||||
<button id="newStoryBtn" type="button">New story</button>
|
|
||||||
<button id="storyFromSelectionBtn" type="button">Create story from memory</button>
|
<button id="storyFromSelectionBtn" type="button">Create story from memory</button>
|
||||||
|
<button id="addSectionBtn" type="button">Add section</button>
|
||||||
<button id="deleteStoryBtn" class="danger" type="button">Delete story</button>
|
<button id="deleteStoryBtn" class="danger" type="button">Delete story</button>
|
||||||
</div>
|
</div>
|
||||||
<h3 style="margin-top:14px">Story flow</h3>
|
<h3 style="margin-top:14px">Story flow</h3>
|
||||||
<div id="storyFlow" class="story-flow-list"></div>
|
<div id="storyFlow" class="story-flow-list"></div>
|
||||||
<div id="storyDropZone" class="story-drop-zone">Drag memories here to add them to this story.</div>
|
<div id="storyDropZone" class="story-drop-zone">Drag memories here to add them to this story.</div>
|
||||||
|
<div id="sectionEditor" class="story-cover hidden" style="margin-top:10px">
|
||||||
|
<label>Section title<input id="sectionTitle" /></label>
|
||||||
|
<label style="margin-top:8px">Section writing<textarea id="sectionContent" spellcheck="true"></textarea></label>
|
||||||
|
<div class="actions" style="margin-top:8px">
|
||||||
|
<button id="saveSectionBtn" type="button">Save section</button>
|
||||||
|
<button id="closeSectionBtn" type="button">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<h3 style="margin-top:14px">Memory pool</h3>
|
<h3 style="margin-top:14px">Memory pool</h3>
|
||||||
<div id="memoryPool" class="memory-pool"></div>
|
<div id="memoryPool" class="memory-pool"></div>
|
||||||
</section>
|
</section>
|
||||||
@@ -1715,8 +1724,8 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
<h2>Memory editor</h2>
|
<h2>Memory editor</h2>
|
||||||
<div class="editor-grid">
|
<div class="editor-grid">
|
||||||
<label class="full">Name<input id="title" /></label>
|
<label class="full">Name<input id="title" /></label>
|
||||||
<label>Runtime kind<select id="type"></select></label>
|
<label>Memory type<select id="type"></select></label>
|
||||||
<label>Canonical class<select id="contentClass"></select></label>
|
<label>Behavior<input id="canonicalUse" placeholder="where and how this memory appears" /></label>
|
||||||
<label>Depth<select id="familyLayer"></select></label>
|
<label>Depth<select id="familyLayer"></select></label>
|
||||||
<label>Characters<input id="characters" placeholder="lima, aphy" /></label>
|
<label>Characters<input id="characters" placeholder="lima, aphy" /></label>
|
||||||
<div id="characterTags" class="full pills"></div>
|
<div id="characterTags" class="full pills"></div>
|
||||||
@@ -1725,24 +1734,24 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
<label>Rarity<select id="rarity"></select></label>
|
<label>Rarity<select id="rarity"></select></label>
|
||||||
<label>Discovery<select id="discoveryDifficulty" placeholder="gentle, patient, hidden" /></label>
|
<label>Discovery<select id="discoveryDifficulty" placeholder="gentle, patient, hidden" /></label>
|
||||||
<label>Mystery<input id="mysteryLevel" placeholder="quiet, strange, deep" /></label>
|
<label>Mystery<input id="mysteryLevel" placeholder="quiet, strange, deep" /></label>
|
||||||
<label>Resonance<input id="resonanceScore" type="number" min="1" max="10" /></label>
|
<label>Glow<input id="resonanceScore" type="number" min="1" max="10" /></label>
|
||||||
<label>Where it appears<input id="pageLocation" placeholder="/play/lima-note.html" /></label>
|
<label>Optional route<input id="pageLocation" placeholder="/play/lima-note.html" /></label>
|
||||||
<label class="full">Trigger path<input id="triggerConditions" placeholder="search: lima, hover, idle 90 seconds" /></label>
|
<label class="full">Trigger path<input id="triggerConditions" placeholder="search: lima, hover, idle 90 seconds" /></label>
|
||||||
<label class="full">Content text<textarea id="content" spellcheck="true"></textarea></label>
|
<label class="full">Content text<textarea id="content" spellcheck="true"></textarea></label>
|
||||||
<label>Reusable surfaces<input id="surfaces" placeholder="tooltip, story, observatory" /></label>
|
<label>Can appear as<input id="surfaces" placeholder="tooltip, story, observatory" /></label>
|
||||||
<label>Observatory role<select id="observatoryRole"><option value="ambient">Ambient light</option><option value="node">Node</option><option value="event">Triggered event</option><option value="guide">Layer guide</option></select></label>
|
<label>Observatory role<select id="observatoryRole"><option value="ambient">Ambient light</option><option value="node">Node</option><option value="event">Triggered event</option><option value="guide">Layer guide</option></select></label>
|
||||||
<label class="full">Canonical use<input id="canonicalUse" placeholder="short reusable whisper, observatory scene, search trigger..." /></label>
|
|
||||||
<label>Symbols<input id="symbols" placeholder="ring, tea, crayon sun" /></label>
|
<label>Symbols<input id="symbols" placeholder="ring, tea, crayon sun" /></label>
|
||||||
<label>Story hint<input id="narrativeArcs" placeholder="optional legacy arc note" /></label>
|
<label>Story hint<input id="narrativeArcs" placeholder="optional theme note" /></label>
|
||||||
<label>Tags<input id="tags" placeholder="warm, october" /></label>
|
<label>Tags<input id="tags" placeholder="warm, october" /></label>
|
||||||
<label>Role<input id="emotionalRole" placeholder="reassurance, invitation, warning" /></label>
|
<label>Role<input id="emotionalRole" placeholder="reassurance, invitation, warning" /></label>
|
||||||
<label>Enabled<select id="enabled"><option value="true">Awake</option><option value="false">Resting</option></select></label>
|
<label>Enabled<select id="enabled"><option value="true">Awake</option><option value="false">Resting</option></select></label>
|
||||||
<label>CSS hooks<input id="cssClassHooks" /></label>
|
|
||||||
<label>Audio<input id="audioSettings" /></label>
|
|
||||||
<label>Animation<input id="animationTrigger" /></label>
|
|
||||||
<label class="full">Private notes<textarea id="notes"></textarea></label>
|
<label class="full">Private notes<textarea id="notes"></textarea></label>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden" aria-hidden="true">
|
<div class="hidden" aria-hidden="true">
|
||||||
|
<select id="contentClass"></select>
|
||||||
|
<input id="cssClassHooks" />
|
||||||
|
<input id="audioSettings" />
|
||||||
|
<input id="animationTrigger" />
|
||||||
<input id="continuationLinks" />
|
<input id="continuationLinks" />
|
||||||
<input id="echoes" />
|
<input id="echoes" />
|
||||||
<input id="thematicLinks" />
|
<input id="thematicLinks" />
|
||||||
@@ -1775,7 +1784,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
const draftKey = "hiddenNarrativeObservatoryDraft:v1";
|
const draftKey = "hiddenNarrativeObservatoryDraft:v1";
|
||||||
const WORLD = { width: 4600, height: 3300, cx: 2300, cy: 1650 };
|
const WORLD = { width: 4600, height: 3300, cx: 2300, cy: 1650 };
|
||||||
const SCREEN = { width: 1000, height: 760 };
|
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 state = { entries: [], stories: [], meta: {}, mode: "graph", selectedId: "", selectedStoryId: "", editingSectionId: "", 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","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 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 storyFields = ["storyTitle","storyDescription","storyTone","storyDiscovery","storyCharacters","storySymbols","storyMarkers","storyLayers","storyUnlock","storyHidden"];
|
||||||
const $ = (id) => document.getElementById(id);
|
const $ = (id) => document.getElementById(id);
|
||||||
@@ -1797,6 +1806,11 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
function storiesForEntry(entryId) {
|
function storiesForEntry(entryId) {
|
||||||
return state.stories.filter((story) => (story.nodes || []).includes(entryId));
|
return state.stories.filter((story) => (story.nodes || []).includes(entryId));
|
||||||
}
|
}
|
||||||
|
function storyItems(story) {
|
||||||
|
if (!story) return [];
|
||||||
|
if (Array.isArray(story.items) && story.items.length) return story.items;
|
||||||
|
return (story.nodes || []).map((id) => ({ kind: "memory", id }));
|
||||||
|
}
|
||||||
function entryById(id) {
|
function entryById(id) {
|
||||||
return state.entries.find((entry) => entry.id === id);
|
return state.entries.find((entry) => entry.id === id);
|
||||||
}
|
}
|
||||||
@@ -2126,6 +2140,8 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
}
|
}
|
||||||
function storyFromForm() {
|
function storyFromForm() {
|
||||||
const current = storyById() || {};
|
const current = storyById() || {};
|
||||||
|
const items = storyItems(current);
|
||||||
|
const nodes = items.filter((item) => item.kind !== "section").map((item) => item.id).filter(Boolean);
|
||||||
return {
|
return {
|
||||||
...current,
|
...current,
|
||||||
id: current.id || `story-${slugify($("storyTitle").value || "untitled-story")}-${Date.now()}`,
|
id: current.id || `story-${slugify($("storyTitle").value || "untitled-story")}-${Date.now()}`,
|
||||||
@@ -2134,7 +2150,8 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
tone: $("storyTone").value || "warm",
|
tone: $("storyTone").value || "warm",
|
||||||
characters: normalizeCharacters($("storyCharacters").value),
|
characters: normalizeCharacters($("storyCharacters").value),
|
||||||
symbols: splitList($("storySymbols").value),
|
symbols: splitList($("storySymbols").value),
|
||||||
nodes: current.nodes || [],
|
items,
|
||||||
|
nodes,
|
||||||
discoveryStyle: $("storyDiscovery").value || "gradual",
|
discoveryStyle: $("storyDiscovery").value || "gradual",
|
||||||
layerAffinity: splitList($("storyLayers").value).map((item) => Number(item)).filter((item) => Number.isInteger(item) && item >= 0 && item <= 5),
|
layerAffinity: splitList($("storyLayers").value).map((item) => Number(item)).filter((item) => Number.isInteger(item) && item >= 0 && item <= 5),
|
||||||
unlockConditions: splitList($("storyUnlock").value),
|
unlockConditions: splitList($("storyUnlock").value),
|
||||||
@@ -2157,6 +2174,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
$("storyCover").innerHTML = "<span class='subtle'>Create a story to shape a route through the observatory.</span>";
|
$("storyCover").innerHTML = "<span class='subtle'>Create a story to shape a route through the observatory.</span>";
|
||||||
storyFields.forEach((id) => { if ($(id)) $(id).value = ""; });
|
storyFields.forEach((id) => { if ($(id)) $(id).value = ""; });
|
||||||
$("storyFlow").innerHTML = "";
|
$("storyFlow").innerHTML = "";
|
||||||
|
closeSectionEditor();
|
||||||
renderMemoryPool();
|
renderMemoryPool();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2257,13 +2275,13 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
function storyEntries(story) {
|
function storyEntries(story) {
|
||||||
return (story?.nodes || []).map(entryById).filter(Boolean);
|
return storyItems(story).filter((item) => item.kind !== "section").map((item) => entryById(item.id)).filter(Boolean);
|
||||||
}
|
}
|
||||||
function storySequencePairs(entries) {
|
function storySequencePairs(entries) {
|
||||||
const visible = new Set(entries.map((entry) => entry.id));
|
const visible = new Set(entries.map((entry) => entry.id));
|
||||||
const pairs = [];
|
const pairs = [];
|
||||||
filteredStories().forEach((story) => {
|
filteredStories().forEach((story) => {
|
||||||
const nodes = (story.nodes || []).filter((id) => visible.has(id));
|
const nodes = storyItems(story).filter((item) => item.kind !== "section").map((item) => item.id).filter((id) => visible.has(id));
|
||||||
nodes.slice(0, -1).forEach((source, index) => pairs.push({ source, target: nodes[index + 1], kind: "story-path", storyId: story.id }));
|
nodes.slice(0, -1).forEach((source, index) => pairs.push({ source, target: nodes[index + 1], kind: "story-path", storyId: story.id }));
|
||||||
});
|
});
|
||||||
return pairs;
|
return pairs;
|
||||||
@@ -2826,7 +2844,11 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
story = storyById();
|
story = storyById();
|
||||||
}
|
}
|
||||||
[sourceId, targetId].forEach((id) => {
|
[sourceId, targetId].forEach((id) => {
|
||||||
if (story && !story.nodes.includes(id)) story.nodes.push(id);
|
if (story && !(story.nodes || []).includes(id)) {
|
||||||
|
story.items = storyItems(story);
|
||||||
|
story.items.push({ kind: "memory", id });
|
||||||
|
story.nodes = story.items.filter((item) => item.kind !== "section").map((item) => item.id);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
state.selectedId = sourceId;
|
state.selectedId = sourceId;
|
||||||
rememberDraft();
|
rememberDraft();
|
||||||
@@ -3102,7 +3124,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
snapshotExploration("focus thread");
|
snapshotExploration("focus thread");
|
||||||
state.history.push({ focusCluster: state.focusCluster, focusIds: state.focusIds ? [...state.focusIds] : null, focusLabel: state.focusLabel, filters: filterState(), camera: { ...state.camera }, zoom: state.zoom, selectedId: state.selectedId });
|
state.history.push({ focusCluster: state.focusCluster, focusIds: state.focusIds ? [...state.focusIds] : null, focusLabel: state.focusLabel, filters: filterState(), camera: { ...state.camera }, zoom: state.zoom, selectedId: state.selectedId });
|
||||||
state.focusCluster = "";
|
state.focusCluster = "";
|
||||||
state.focusIds = new Set(story.nodes || []);
|
state.focusIds = new Set(storyItems(story).filter((item) => item.kind !== "section").map((item) => item.id));
|
||||||
state.focusLabel = `Story: ${story.title || story.id}`;
|
state.focusLabel = `Story: ${story.title || story.id}`;
|
||||||
render();
|
render();
|
||||||
fitCameraTo(boundsForEntries(visibleScope().entries), true);
|
fitCameraTo(boundsForEntries(visibleScope().entries), true);
|
||||||
@@ -3180,19 +3202,25 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
}
|
}
|
||||||
const characters = (story.characters || []).map((character) => `<span class="avatar-pill pill">${avatarImg(character, "tiny")}${html(characterLabel(character))}</span>`).join("");
|
const characters = (story.characters || []).map((character) => `<span class="avatar-pill pill">${avatarImg(character, "tiny")}${html(characterLabel(character))}</span>`).join("");
|
||||||
$("storyCover").innerHTML = `<strong>${html(story.title)}</strong><p class="subtle">${html(story.description || "A quiet route waiting for a summary.")}</p><div class="pills">${characters}<span class="pill">${html(story.tone || "warm")}</span><span class="pill">${html(story.discoveryStyle || "gradual")}</span>${story.hidden ? "<span class='pill'>hidden</span>" : ""}${(story.markers || []).map((item) => `<span class="pill">${html(item)}</span>`).join("")}</div>`;
|
$("storyCover").innerHTML = `<strong>${html(story.title)}</strong><p class="subtle">${html(story.description || "A quiet route waiting for a summary.")}</p><div class="pills">${characters}<span class="pill">${html(story.tone || "warm")}</span><span class="pill">${html(story.discoveryStyle || "gradual")}</span>${story.hidden ? "<span class='pill'>hidden</span>" : ""}${(story.markers || []).map((item) => `<span class="pill">${html(item)}</span>`).join("")}</div>`;
|
||||||
const nodes = storyEntries(story);
|
const items = storyItems(story);
|
||||||
$("storyFlow").innerHTML = nodes.length ? nodes.map((entry, index) => {
|
$("storyFlow").innerHTML = items.length ? items.map((item, index) => {
|
||||||
const character = primaryCharacterForEntry(entry);
|
if (item.kind === "section") {
|
||||||
return `<div class="story-flow-item" draggable="true" data-node="${html(entry.id)}"><span class="story-step">${index + 1}</span><span><strong>${html(entry.title)}</strong><br><span class="subtle">${html(entry.emotionalTone || "warm")} / ${html(layerName(depth(entry)))}</span></span><button type="button" data-remove="${html(entry.id)}">Remove</button></div>`;
|
return `<div class="story-flow-item section" draggable="true" data-item="${html(item.id)}"><span class="story-step">${index + 1}</span><span><strong>${html(item.title || "Story section")}</strong><br><span class="subtle">story-only section / ${html(item.tone || story.tone || "warm")}</span></span><span class="actions"><button type="button" data-edit-section="${html(item.id)}">Edit</button><button type="button" data-remove-section="${html(item.id)}">Remove</button></span></div>`;
|
||||||
}).join("") : "<div class='subtle'>No memories in this story yet.</div>";
|
}
|
||||||
|
const entry = entryById(item.id);
|
||||||
|
if (!entry) return "";
|
||||||
|
return `<div class="story-flow-item" draggable="true" data-item="${html(item.id)}"><span class="story-step">${index + 1}</span><span><strong>${html(entry.title)}</strong><br><span class="subtle">${html(entry.type || "memory")} / ${html(entry.emotionalTone || "warm")}</span></span><button type="button" data-remove="${html(entry.id)}">Remove</button></div>`;
|
||||||
|
}).join("") : "<div class='subtle'>No memories or sections in this story yet.</div>";
|
||||||
$("storyFlow").querySelectorAll("[data-remove]").forEach((button) => button.addEventListener("click", () => removeNodeFromStory(button.dataset.remove)));
|
$("storyFlow").querySelectorAll("[data-remove]").forEach((button) => button.addEventListener("click", () => removeNodeFromStory(button.dataset.remove)));
|
||||||
|
$("storyFlow").querySelectorAll("[data-edit-section]").forEach((button) => button.addEventListener("click", () => editStorySection(button.dataset.editSection)));
|
||||||
|
$("storyFlow").querySelectorAll("[data-remove-section]").forEach((button) => button.addEventListener("click", () => removeStoryItem(button.dataset.removeSection)));
|
||||||
$("storyFlow").querySelectorAll("[draggable='true']").forEach((item) => {
|
$("storyFlow").querySelectorAll("[draggable='true']").forEach((item) => {
|
||||||
item.addEventListener("dragstart", (event) => event.dataTransfer.setData("text/story-node", item.dataset.node));
|
item.addEventListener("dragstart", (event) => event.dataTransfer.setData("text/story-item", item.dataset.item));
|
||||||
item.addEventListener("dragover", (event) => event.preventDefault());
|
item.addEventListener("dragover", (event) => event.preventDefault());
|
||||||
item.addEventListener("drop", (event) => {
|
item.addEventListener("drop", (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const source = event.dataTransfer.getData("text/story-node") || event.dataTransfer.getData("text/memory-id");
|
const source = event.dataTransfer.getData("text/story-item") || event.dataTransfer.getData("text/story-node") || event.dataTransfer.getData("text/memory-id");
|
||||||
if (source) moveNodeInStory(source, item.dataset.node);
|
if (source) moveStoryItem(source, item.dataset.item);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -3218,12 +3246,12 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
button.addEventListener("dragstart", (event) => event.dataTransfer.setData("text/memory-id", button.dataset.id));
|
button.addEventListener("dragstart", (event) => event.dataTransfer.setData("text/memory-id", button.dataset.id));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function newEntry(type = "quote") {
|
function newEntry(type = "tooltip") {
|
||||||
snapshot();
|
snapshot();
|
||||||
const now = new Date().toISOString().slice(0, 10);
|
const now = new Date().toISOString().slice(0, 10);
|
||||||
const entry = {
|
const entry = {
|
||||||
id: `memory-${Date.now()}`,
|
id: `memory-${Date.now()}`,
|
||||||
title: "Untitled fragment",
|
title: "Untitled memory",
|
||||||
type,
|
type,
|
||||||
contentClass: state.meta.typeArchitecture?.[type]?.contentClass || "fragment",
|
contentClass: state.meta.typeArchitecture?.[type]?.contentClass || "fragment",
|
||||||
content: "",
|
content: "",
|
||||||
@@ -3273,6 +3301,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
tone: entry?.emotionalTone || "warm",
|
tone: entry?.emotionalTone || "warm",
|
||||||
characters: entry?.characters?.length ? [...entry.characters] : ["z"],
|
characters: entry?.characters?.length ? [...entry.characters] : ["z"],
|
||||||
symbols: entry?.symbols ? [...entry.symbols] : [],
|
symbols: entry?.symbols ? [...entry.symbols] : [],
|
||||||
|
items: entry ? [{ kind: "memory", id: entry.id }] : [],
|
||||||
nodes: entry ? [entry.id] : [],
|
nodes: entry ? [entry.id] : [],
|
||||||
discoveryStyle: "gradual",
|
discoveryStyle: "gradual",
|
||||||
layerAffinity: entry ? [depth(entry)] : [],
|
layerAffinity: entry ? [depth(entry)] : [],
|
||||||
@@ -3301,8 +3330,11 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
}
|
}
|
||||||
function addNodeToStory(nodeId) {
|
function addNodeToStory(nodeId) {
|
||||||
const story = storyById();
|
const story = storyById();
|
||||||
if (!story || !nodeId || story.nodes.includes(nodeId)) return;
|
if (!story || !nodeId || (story.nodes || []).includes(nodeId)) return;
|
||||||
snapshot();
|
snapshot();
|
||||||
|
story.items = storyItems(story);
|
||||||
|
story.items.push({ kind: "memory", id: nodeId });
|
||||||
|
story.nodes = story.nodes || [];
|
||||||
story.nodes.push(nodeId);
|
story.nodes.push(nodeId);
|
||||||
const entry = entryById(nodeId);
|
const entry = entryById(nodeId);
|
||||||
if (entry) {
|
if (entry) {
|
||||||
@@ -3321,23 +3353,83 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
const story = storyById();
|
const story = storyById();
|
||||||
if (!story) return;
|
if (!story) return;
|
||||||
snapshot();
|
snapshot();
|
||||||
story.nodes = (story.nodes || []).filter((id) => id !== nodeId);
|
story.items = storyItems(story).filter((item) => !(item.kind !== "section" && item.id === nodeId));
|
||||||
|
story.nodes = story.items.filter((item) => item.kind !== "section").map((item) => item.id);
|
||||||
|
rememberDraft();
|
||||||
|
fillStoryForm(story);
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
function removeStoryItem(itemId) {
|
||||||
|
const story = storyById();
|
||||||
|
if (!story) return;
|
||||||
|
snapshot();
|
||||||
|
story.items = storyItems(story).filter((item) => item.id !== itemId);
|
||||||
|
story.nodes = story.items.filter((item) => item.kind !== "section").map((item) => item.id);
|
||||||
rememberDraft();
|
rememberDraft();
|
||||||
fillStoryForm(story);
|
fillStoryForm(story);
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
function moveNodeInStory(sourceId, targetId) {
|
function moveNodeInStory(sourceId, targetId) {
|
||||||
|
moveStoryItem(sourceId, targetId);
|
||||||
|
}
|
||||||
|
function moveStoryItem(sourceId, targetId) {
|
||||||
const story = storyById();
|
const story = storyById();
|
||||||
if (!story || !sourceId) return;
|
if (!story || !sourceId) return;
|
||||||
snapshot();
|
snapshot();
|
||||||
story.nodes = (story.nodes || []).filter((id) => id !== sourceId);
|
const sourceIsExisting = storyItems(story).some((item) => item.id === sourceId);
|
||||||
const targetIndex = story.nodes.indexOf(targetId);
|
const sourceItem = sourceIsExisting ? storyItems(story).find((item) => item.id === sourceId) : { kind: "memory", id: sourceId };
|
||||||
if (targetIndex >= 0) story.nodes.splice(targetIndex, 0, sourceId);
|
story.items = storyItems(story).filter((item) => item.id !== sourceId);
|
||||||
else story.nodes.push(sourceId);
|
const targetIndex = story.items.findIndex((item) => item.id === targetId);
|
||||||
|
if (targetIndex >= 0) story.items.splice(targetIndex, 0, sourceItem);
|
||||||
|
else story.items.push(sourceItem);
|
||||||
|
story.nodes = story.items.filter((item) => item.kind !== "section").map((item) => item.id);
|
||||||
rememberDraft();
|
rememberDraft();
|
||||||
fillStoryForm(story);
|
fillStoryForm(story);
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
|
function addStorySection() {
|
||||||
|
const story = storyById();
|
||||||
|
if (!story) return setStatus("create or select a story first.");
|
||||||
|
snapshot();
|
||||||
|
story.items = storyItems(story);
|
||||||
|
const section = { kind: "section", id: `section-${Date.now()}`, title: "Quiet section", content: "", tone: story.tone || "warm" };
|
||||||
|
story.items.push(section);
|
||||||
|
story.nodes = story.items.filter((item) => item.kind !== "section").map((item) => item.id);
|
||||||
|
state.editingSectionId = section.id;
|
||||||
|
rememberDraft();
|
||||||
|
fillStoryForm(story);
|
||||||
|
openSectionEditor(section.id);
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
function editStorySection(sectionId) {
|
||||||
|
openSectionEditor(sectionId);
|
||||||
|
}
|
||||||
|
function openSectionEditor(sectionId) {
|
||||||
|
const story = storyById();
|
||||||
|
const section = storyItems(story).find((item) => item.id === sectionId && item.kind === "section");
|
||||||
|
if (!story || !section) return;
|
||||||
|
state.editingSectionId = section.id;
|
||||||
|
$("sectionTitle").value = section.title || "";
|
||||||
|
$("sectionContent").value = section.content || "";
|
||||||
|
$("sectionEditor").classList.remove("hidden");
|
||||||
|
}
|
||||||
|
function saveStorySection() {
|
||||||
|
const story = storyById();
|
||||||
|
const section = storyItems(story).find((item) => item.id === state.editingSectionId && item.kind === "section");
|
||||||
|
if (!story || !section) return closeSectionEditor();
|
||||||
|
snapshot();
|
||||||
|
section.title = $("sectionTitle").value.trim() || "Story section";
|
||||||
|
section.content = $("sectionContent").value;
|
||||||
|
section.tone = story.tone || section.tone || "warm";
|
||||||
|
story.items = storyItems(story);
|
||||||
|
rememberDraft();
|
||||||
|
fillStoryForm(story);
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
function closeSectionEditor() {
|
||||||
|
state.editingSectionId = "";
|
||||||
|
$("sectionEditor")?.classList.add("hidden");
|
||||||
|
}
|
||||||
function duplicateEntry() {
|
function duplicateEntry() {
|
||||||
const entry = currentEntry();
|
const entry = currentEntry();
|
||||||
if (!entry) return;
|
if (!entry) return;
|
||||||
@@ -3354,6 +3446,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
state.entries = state.entries.filter((item) => item.id !== entry.id);
|
state.entries = state.entries.filter((item) => item.id !== entry.id);
|
||||||
state.stories.forEach((story) => {
|
state.stories.forEach((story) => {
|
||||||
story.nodes = (story.nodes || []).filter((id) => id !== entry.id);
|
story.nodes = (story.nodes || []).filter((id) => id !== entry.id);
|
||||||
|
story.items = storyItems(story).filter((item) => item.kind === "section" || item.id !== entry.id);
|
||||||
});
|
});
|
||||||
state.entries.forEach((item) => {
|
state.entries.forEach((item) => {
|
||||||
["continuationLinks","echoes","thematicLinks","symbolicLinks","triggerLinks","parentLinks","childLinks","mirroredEntries"].forEach((key) => {
|
["continuationLinks","echoes","thematicLinks","symbolicLinks","triggerLinks","parentLinks","childLinks","mirroredEntries"].forEach((key) => {
|
||||||
@@ -3548,6 +3641,9 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
$("newBtn").onclick = () => newEntry();
|
$("newBtn").onclick = () => newEntry();
|
||||||
$("newStoryBtn").onclick = () => newStory();
|
$("newStoryBtn").onclick = () => newStory();
|
||||||
$("storyFromSelectionBtn").onclick = () => newStory(state.selectedId);
|
$("storyFromSelectionBtn").onclick = () => newStory(state.selectedId);
|
||||||
|
$("addSectionBtn").onclick = addStorySection;
|
||||||
|
$("saveSectionBtn").onclick = saveStorySection;
|
||||||
|
$("closeSectionBtn").onclick = closeSectionEditor;
|
||||||
$("deleteStoryBtn").onclick = deleteStory;
|
$("deleteStoryBtn").onclick = deleteStory;
|
||||||
$("saveBtn").onclick = save;
|
$("saveBtn").onclick = save;
|
||||||
$("zoomOutBtn").onclick = () => zoomBy(-1);
|
$("zoomOutBtn").onclick = () => zoomBy(-1);
|
||||||
@@ -3576,7 +3672,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
$("storyDropZone").addEventListener("dragover", (event) => event.preventDefault());
|
$("storyDropZone").addEventListener("dragover", (event) => event.preventDefault());
|
||||||
$("storyDropZone").addEventListener("drop", (event) => {
|
$("storyDropZone").addEventListener("drop", (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const id = event.dataTransfer.getData("text/memory-id") || event.dataTransfer.getData("text/story-node");
|
const id = event.dataTransfer.getData("text/memory-id") || event.dataTransfer.getData("text/story-node") || event.dataTransfer.getData("text/story-item");
|
||||||
if (id) addNodeToStory(id);
|
if (id) addNodeToStory(id);
|
||||||
});
|
});
|
||||||
document.addEventListener("click", (event) => {
|
document.addEventListener("click", (event) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user