memory observatory redesign
All checks were successful
Build Authoring Service / build (push) Successful in 8s
All checks were successful
Build Authoring Service / build (push) Successful in 8s
This commit is contained in:
@@ -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.
|
||||
|
||||
82
docs/hidden-memory-architecture.md
Normal file
82
docs/hidden-memory-architecture.md
Normal file
@@ -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.
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -1609,7 +1609,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
<select id="rarityFilter"></select>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button id="newBtn" type="button">New memory</button>
|
||||
<button id="newBtn" type="button">New fragment</button>
|
||||
<button id="saveBtn" class="primary" type="button">Save constellation</button>
|
||||
</div>
|
||||
<div class="actions" style="margin-top:8px">
|
||||
@@ -1715,7 +1715,8 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
<h2>Memory editor</h2>
|
||||
<div class="editor-grid">
|
||||
<label class="full">Name<input id="title" /></label>
|
||||
<label>Kind<select id="type"></select></label>
|
||||
<label>Runtime kind<select id="type"></select></label>
|
||||
<label>Canonical class<select id="contentClass"></select></label>
|
||||
<label>Depth<select id="familyLayer"></select></label>
|
||||
<label>Characters<input id="characters" placeholder="lima, aphy" /></label>
|
||||
<div id="characterTags" class="full pills"></div>
|
||||
@@ -1727,7 +1728,10 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
<label>Resonance<input id="resonanceScore" type="number" min="1" max="10" /></label>
|
||||
<label>Where it appears<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">Memory 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>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>Story hint<input id="narrativeArcs" placeholder="optional legacy arc note" /></label>
|
||||
<label>Tags<input id="tags" placeholder="warm, october" /></label>
|
||||
@@ -1757,7 +1761,8 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
<section class="panel">
|
||||
<h2>Architecture</h2>
|
||||
<div class="subtle">
|
||||
<p><strong>Story model:</strong> stories are first-class emotional routes. Memories can belong to multiple stories, but routes stay readable and authored in order.</p>
|
||||
<p><strong>Canonical model:</strong> fragments are tiny reusable lines, memories are richer observatory moments, interactions are triggers, lore is world material, and stories are curated routes through them.</p>
|
||||
<p><strong>Story model:</strong> stories are first-class emotional routes. Entries can belong to multiple stories, but routes stay readable and authored in order.</p>
|
||||
<p><strong>Legacy links:</strong> old continuations, echoes, and symbolic links are retained for migration only. New authoring happens through Story Builder.</p>
|
||||
<p><strong>Storage:</strong> the friendly graph lives in <code>assets/content/hidden-details.json</code>; the live site still receives generated constants in <code>assets/scripts/hidden-details.js</code>.</p>
|
||||
</div>
|
||||
@@ -1771,7 +1776,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
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"""<!doctype html>
|
||||
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"""<!doctype html>
|
||||
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"""<!doctype html>
|
||||
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"""<!doctype html>
|
||||
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"""<!doctype html>
|
||||
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"""<!doctype html>
|
||||
$("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"""<!doctype html>
|
||||
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"""<!doctype html>
|
||||
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"""<!doctype html>
|
||||
function renderPreview(entry) {
|
||||
const body = html(entry.content || "").replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
||||
const avatarPills = charactersForEntry(entry).map((character) => `<span class="avatar-pill pill" style="--avatar-glow:${html(avatarGlow(character))}">${avatarImg(character, "tiny", avatarExpressionForEntry(entry))}${html(characterLabel(character))}</span>`).join("");
|
||||
$("preview").innerHTML = `${body || "<span class='subtle'>This memory is waiting for words.</span>"}<div class="pills" style="margin-top:10px">${avatarPills}<span class="pill">${html(layerName(depth(entry)))}</span><span class="pill">${html(entry.emotionalTone)}</span><span class="pill">${html(entry.rarity)}</span>${(entry.symbols || []).map((item) => `<span class="pill">${html(item)}</span>`).join("")}</div>`;
|
||||
$("preview").innerHTML = `${body || "<span class='subtle'>This entry is waiting for words.</span>"}<div class="pills" style="margin-top:10px">${avatarPills}<span class="pill">${html(entry.contentClass || "fragment")}</span><span class="pill">${html(entry.observatoryRole || "ambient")}</span><span class="pill">${html(layerName(depth(entry)))}</span><span class="pill">${html(entry.emotionalTone)}</span><span class="pill">${html(entry.rarity)}</span>${(entry.surfaces || []).map((item) => `<span class="pill">${html(item)}</span>`).join("")}${(entry.symbols || []).map((item) => `<span class="pill">${html(item)}</span>`).join("")}</div>`;
|
||||
}
|
||||
function renderConnections(entry) {
|
||||
const stories = storiesForEntry(entry.id);
|
||||
@@ -3201,13 +3223,17 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
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"""<!doctype html>
|
||||
function populateControls() {
|
||||
const opt = (value, label = value) => `<option value="${html(value)}">${html(label || "All")}</option>`;
|
||||
$("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"""<!doctype html>
|
||||
$(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(); });
|
||||
|
||||
Reference in New Issue
Block a user