This commit is contained in:
@@ -149,6 +149,14 @@ CHARACTER_REGISTRY = {
|
||||
"aliases": ["Young Z", "young z", "young-z", "young_z", "young"],
|
||||
"territoryColor": "#d8a95a",
|
||||
"symbol": "Y",
|
||||
"avatar": {
|
||||
"shape": "sun-child",
|
||||
"accent": "#f1bc5b",
|
||||
"shadow": "#7a5630",
|
||||
"mood": "excited",
|
||||
"motif": "crayon sun",
|
||||
"description": "small crayon-sun spirit with oversized curiosity",
|
||||
},
|
||||
"motifs": ["crayon sun", "blanket cape", "childhood desk"],
|
||||
"themes": ["childhood", "play", "memory", "safety"],
|
||||
"affinities": ["z", "future z", "aphy", "lima"],
|
||||
@@ -159,6 +167,14 @@ CHARACTER_REGISTRY = {
|
||||
"aliases": ["Z", "z", "zaine"],
|
||||
"territoryColor": "#d6c38a",
|
||||
"symbol": "Z",
|
||||
"avatar": {
|
||||
"shape": "traveler",
|
||||
"accent": "#d6c38a",
|
||||
"shadow": "#5d533a",
|
||||
"mood": "receptive",
|
||||
"motif": "small lantern",
|
||||
"description": "present observer spirit, quiet and receptive",
|
||||
},
|
||||
"motifs": ["archive", "website", "home"],
|
||||
"themes": ["selfhood", "return", "making"],
|
||||
"affinities": ["lima", "young z", "future z"],
|
||||
@@ -169,6 +185,14 @@ CHARACTER_REGISTRY = {
|
||||
"aliases": ["Aphy", "aphy", "aphy_bot", "aphy bot"],
|
||||
"territoryColor": "#7fb089",
|
||||
"symbol": "A",
|
||||
"avatar": {
|
||||
"shape": "terminal",
|
||||
"accent": "#7fd0a0",
|
||||
"shadow": "#315944",
|
||||
"mood": "curious",
|
||||
"motif": "terminal cursor",
|
||||
"description": "curious tiny system companion with a soft terminal glow",
|
||||
},
|
||||
"motifs": ["console", "diagnostic", "backup"],
|
||||
"themes": ["humor", "systems", "care through tools"],
|
||||
"affinities": ["z", "lima", "sensei chi"],
|
||||
@@ -179,6 +203,14 @@ CHARACTER_REGISTRY = {
|
||||
"aliases": ["Lima", "lima"],
|
||||
"territoryColor": "#d06b78",
|
||||
"symbol": "L",
|
||||
"avatar": {
|
||||
"shape": "hearth",
|
||||
"accent": "#e78f8d",
|
||||
"shadow": "#6d3f43",
|
||||
"mood": "warm",
|
||||
"motif": "kitchen light",
|
||||
"description": "warm grounding spirit with calm eyes and a soft light",
|
||||
},
|
||||
"motifs": ["warmth", "kitchen light", "ring"],
|
||||
"themes": ["love", "home", "grounding"],
|
||||
"affinities": ["z", "aphy", "future z", "young z"],
|
||||
@@ -189,6 +221,14 @@ CHARACTER_REGISTRY = {
|
||||
"aliases": ["Sensei Chi", "sensei chi", "sensei-chi", "sensei_chi", "sensei"],
|
||||
"territoryColor": "#75a9bd",
|
||||
"symbol": "S",
|
||||
"avatar": {
|
||||
"shape": "sage",
|
||||
"accent": "#84b8c8",
|
||||
"shadow": "#36566a",
|
||||
"mood": "smiling",
|
||||
"motif": "tea lantern",
|
||||
"description": "patient elderly spirit in simple robes",
|
||||
},
|
||||
"motifs": ["tea", "garden", "quiet lesson"],
|
||||
"themes": ["reflection", "patience", "wisdom"],
|
||||
"affinities": ["aphy", "future z"],
|
||||
@@ -199,6 +239,14 @@ CHARACTER_REGISTRY = {
|
||||
"aliases": ["Future Z", "future z", "future-z", "future_z", "future"],
|
||||
"territoryColor": "#a58ac9",
|
||||
"symbol": "F",
|
||||
"avatar": {
|
||||
"shape": "moon",
|
||||
"accent": "#b8a2d7",
|
||||
"shadow": "#51496a",
|
||||
"mood": "reflective",
|
||||
"motif": "distant clock",
|
||||
"description": "older reflective spirit, distant but caring",
|
||||
},
|
||||
"motifs": ["clock", "age 40", "future log"],
|
||||
"themes": ["time", "reassurance", "continuity"],
|
||||
"affinities": ["z", "young z", "lima", "sensei chi"],
|
||||
@@ -1228,6 +1276,7 @@ def generated_hidden_content_block(entries: list[dict[str, Any]]) -> str:
|
||||
" // -----------------------------\n"
|
||||
" // Generated by the hidden narrative authoring page.\n"
|
||||
" // Friendly source of truth: assets/content/hidden-details.json\n\n"
|
||||
f"{js_const('CHARACTER_REGISTRY', CHARACTER_REGISTRY)}\n"
|
||||
f"{js_const('familyLayers', family_layers)}\n"
|
||||
f"{js_const('details', details)}\n"
|
||||
f"{js_const('poems', poems)}\n"
|
||||
@@ -2525,6 +2574,54 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
.filters { display: grid; gap: 9px; margin: 14px 0; }
|
||||
.modebar, .actions, .pills { display: flex; flex-wrap: wrap; gap: 7px; align-items: center; }
|
||||
.modebar button.active { background: rgba(211, 166, 77, 0.26); border-color: var(--gold); }
|
||||
.avatar {
|
||||
width: var(--avatar-size, 24px);
|
||||
height: var(--avatar-size, 24px);
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
flex: 0 0 auto;
|
||||
border-radius: 7px;
|
||||
background: color-mix(in oklab, var(--avatar-color, #d3a64d) 20%, rgba(18, 16, 13, 0.72));
|
||||
box-shadow: 0 0 0 1px color-mix(in oklab, var(--avatar-color, #d3a64d) 42%, transparent), 0 0 14px color-mix(in oklab, var(--avatar-color, #d3a64d) 22%, transparent);
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.avatar svg { width: 100%; height: 100%; display: block; shape-rendering: crispEdges; }
|
||||
.avatar--xs { --avatar-size: 18px; border-radius: 5px; }
|
||||
.avatar--sm { --avatar-size: 24px; }
|
||||
.avatar--md { --avatar-size: 34px; }
|
||||
.avatar--lg { --avatar-size: 52px; border-radius: 10px; }
|
||||
.avatar--breathing { animation: spirit-breathe 5.8s ease-in-out infinite; }
|
||||
.character-chip, .avatar-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border: 1px solid color-mix(in oklab, var(--avatar-color, #d3a64d) 42%, var(--line));
|
||||
border-radius: 999px;
|
||||
padding: 3px 8px 3px 4px;
|
||||
color: #f3ddb2;
|
||||
background: color-mix(in oklab, var(--avatar-color, #d3a64d) 12%, rgba(251, 241, 220, 0.07));
|
||||
font-size: 12px;
|
||||
}
|
||||
.drawer-presence, .character-preview, .timeline-presence {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 7px;
|
||||
margin-top: 9px;
|
||||
}
|
||||
.drawer-heading {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
.drawer-heading h2 { margin: 0; }
|
||||
.guided-spirit {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.pill {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
@@ -2577,6 +2674,9 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
.memory-node:active { cursor: grabbing; }
|
||||
.memory-node text, .node-label text, .cluster-label text { fill: #f7ead3; paint-order: stroke; stroke: rgba(14, 11, 8, 0.82); stroke-width: 3px; stroke-linejoin: round; }
|
||||
.memory-node circle { stroke: rgba(255, 246, 223, 0.82); stroke-width: 1.4; filter: drop-shadow(0 0 7px rgba(255, 232, 177, 0.18)); transition: r 160ms ease, stroke 160ms ease, opacity 160ms ease; }
|
||||
.memory-node .character-halo { fill: none; stroke: var(--node-character-color, rgba(211, 166, 77, 0.46)); stroke-width: 2; opacity: 0.42; stroke-dasharray: 2 5; animation: spirit-shimmer 8s ease-in-out infinite; }
|
||||
.memory-node .node-avatar { opacity: 0; transform: scale(0.72); transition: opacity 220ms ease, transform 220ms ease; }
|
||||
.zoom-2 .memory-node .node-avatar, .zoom-3 .memory-node .node-avatar, .memory-node.selected .node-avatar { opacity: 0.96; transform: scale(1); }
|
||||
.memory-node.selected circle { stroke: #fff3c7; stroke-width: 3; }
|
||||
.memory-node.dragging circle { stroke: #f3cd7a; stroke-width: 3; opacity: 0.72; }
|
||||
.memory-node.compatible circle { stroke: rgba(255, 243, 199, 0.92); stroke-width: 2.4; filter: drop-shadow(0 0 16px rgba(255, 232, 177, 0.46)); }
|
||||
@@ -2600,6 +2700,8 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
}
|
||||
.cluster-node .count { font-size: 18px; font-weight: 850; }
|
||||
.cluster-halo { fill: rgba(211, 166, 77, 0.08); stroke: rgba(211, 166, 77, 0.22); stroke-dasharray: 3 8; }
|
||||
.territory-anchor .cluster-halo { animation: spirit-shimmer 7.5s ease-in-out infinite; }
|
||||
.cluster-avatar { filter: drop-shadow(0 0 10px rgba(255, 232, 177, 0.24)); }
|
||||
.drag-preview-line { stroke: rgba(255, 232, 177, 0.78); stroke-width: 3; stroke-dasharray: 8 8; pointer-events: none; }
|
||||
.drag-preview-halo { fill: rgba(211, 166, 77, 0.1); stroke: rgba(255, 232, 177, 0.58); stroke-width: 2; stroke-dasharray: 4 7; pointer-events: none; }
|
||||
.drag-preview-label rect { fill: rgba(18, 16, 13, 0.86); stroke: rgba(255, 232, 177, 0.42); rx: 7; }
|
||||
@@ -2629,7 +2731,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
gap: 6px;
|
||||
max-width: min(720px, calc(100% - 40px));
|
||||
}
|
||||
.breadcrumb button { min-height: 26px; padding: 0 8px; }
|
||||
.breadcrumb button { min-height: 26px; padding: 0 8px; display: inline-flex; align-items: center; gap: 6px; }
|
||||
.active-state {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
@@ -2744,6 +2846,14 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
0%, 100% { opacity: 0.72; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
@keyframes spirit-breathe {
|
||||
0%, 100% { transform: translateY(0); filter: saturate(0.94); }
|
||||
50% { transform: translateY(-1px); filter: saturate(1.14); }
|
||||
}
|
||||
@keyframes spirit-shimmer {
|
||||
0%, 100% { opacity: 0.28; stroke-dashoffset: 0; }
|
||||
50% { opacity: 0.68; stroke-dashoffset: 11; }
|
||||
}
|
||||
.core-glow { animation: breathe 7s ease-in-out infinite; }
|
||||
@media (max-width: 1120px) {
|
||||
body { overflow: auto; }
|
||||
@@ -2800,11 +2910,11 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
<div class="guide-card">
|
||||
<h2>Guided Exploration</h2>
|
||||
<div class="tour-list">
|
||||
<button data-tour="lima" type="button">show hidden lima 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="unresolved" type="button">show unresolved threads</button>
|
||||
<button data-tour="connected" type="button">show emotionally connected nodes</button>
|
||||
<button data-tour="lima" type="button"><span class="guided-spirit" data-guide-character="lima">show hidden lima memories</span></button>
|
||||
<button data-tour="rare" type="button"><span class="guided-spirit" data-guide-character="sensei chi">show the rarest memories</span></button>
|
||||
<button data-tour="future" type="button"><span class="guided-spirit" data-guide-character="future z">show discoveries tied to future z</span></button>
|
||||
<button data-tour="unresolved" type="button"><span class="guided-spirit" data-guide-character="aphy">show unresolved threads</span></button>
|
||||
<button data-tour="connected" type="button"><span class="guided-spirit" data-guide-character="z">show emotionally connected nodes</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="guide-card">
|
||||
@@ -2843,8 +2953,14 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
|
||||
<aside class="drawer">
|
||||
<section class="panel">
|
||||
<h2 id="drawerTitle">Choose a memory</h2>
|
||||
<div id="drawerSub" class="subtle">The side panel opens from the constellation.</div>
|
||||
<div class="drawer-heading">
|
||||
<div id="drawerAvatar"></div>
|
||||
<div>
|
||||
<h2 id="drawerTitle">Choose a memory</h2>
|
||||
<div id="drawerSub" class="subtle">The side panel opens from the constellation.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="drawerPresence" class="drawer-presence"></div>
|
||||
<div id="preview" class="preview" style="margin-top:10px"></div>
|
||||
</section>
|
||||
<section class="panel" style="margin-top:12px">
|
||||
@@ -2854,6 +2970,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
<label>Kind<select id="type"></select></label>
|
||||
<label>Depth<select id="familyLayer"></select></label>
|
||||
<label>Characters<input id="characters" placeholder="lima, aphy" /></label>
|
||||
<div id="characterPreview" class="character-preview full"></div>
|
||||
<label>Tone<select id="tone"></select></label>
|
||||
<label>Rarity<select id="rarity"></select></label>
|
||||
<label>Discovery<select id="discoveryDifficulty" placeholder="gentle, patient, hidden" /></label>
|
||||
@@ -2910,6 +3027,81 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
function html(value) {
|
||||
return String(value || "").replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[char]));
|
||||
}
|
||||
function characterConfig(character) {
|
||||
return state.meta.characterRegistry?.[character] || state.meta.characterRegistry?.z || { displayLabel: "z", territoryColor: "#d6c38a", symbol: "Z", avatar: { accent: "#d6c38a", shadow: "#5d533a", shape: "traveler", mood: "receptive" } };
|
||||
}
|
||||
function expressionFor(entry, character = "") {
|
||||
const text = [entry?.title, entry?.content, entry?.triggerConditions, entry?.emotionalRole, entry?.mysteryLevel, ...(entry?.symbols || []), ...(entry?.tags || [])].join(" ").toLowerCase();
|
||||
if (character === "aphy" && /error|glitch|terminal|system|diagnostic|bug|css|console/.test(text)) return "glitching";
|
||||
if (character === "lima" && /safe|home|warm|protect|care|eat|rest|kitchen|light/.test(text)) return "concerned";
|
||||
if (character === "young z" && /draw|crayon|game|play|sun|blanket|child|desk/.test(text)) return "drawing";
|
||||
if (character === "future z" && /time|future|age 40|return|ordinary|warning|later/.test(text)) return "reflective";
|
||||
if (character === "sensei chi" && /quiet|tea|patience|lesson|garden|wisdom|still/.test(text)) return "smiling";
|
||||
return characterConfig(character).avatar?.mood || "soft";
|
||||
}
|
||||
function avatarSvg(character, expression = "soft") {
|
||||
const config = characterConfig(character);
|
||||
const avatar = config.avatar || {};
|
||||
const accent = avatar.accent || config.territoryColor || "#d3a64d";
|
||||
const shadow = avatar.shadow || "#5d4632";
|
||||
const symbol = html(config.symbol || "?");
|
||||
const shape = avatar.shape || "traveler";
|
||||
const eye = expression === "reflective" || expression === "smiling" ? "M12 15h3M21 15h3" : expression === "glitching" ? "M11 14h5M21 13h3M22 17h4" : "M12 14h3M21 14h3";
|
||||
const mouth = expression === "concerned" ? "M15 23q3-2 6 0" : expression === "excited" || expression === "drawing" || expression === "smiling" ? "M15 22q3 3 7 0" : "M16 22h6";
|
||||
const motif = {
|
||||
"sun-child": `<path d="M18 3v5M18 28v5M4 18h5M27 18h5M8 8l4 4M28 8l-4 4" stroke="${accent}" stroke-width="2" stroke-linecap="round" opacity=".72"></path>`,
|
||||
terminal: `<path d="M9 8h18v19H9z" fill="none" stroke="${accent}" stroke-width="2" opacity=".62"></path><path d="M13 12l3 3-3 3M18 19h6" stroke="${accent}" stroke-width="2" stroke-linecap="round" opacity=".82"></path>`,
|
||||
sage: `<path d="M12 28c3-5 9-5 13 0" fill="${shadow}" opacity=".48"></path><path d="M25 9c2 2 2 5 0 7" stroke="${accent}" stroke-width="2" fill="none" opacity=".64"></path>`,
|
||||
hearth: `<path d="M18 5c4 4 8 7 8 12 0 6-4 10-8 10s-8-4-8-10c0-5 4-8 8-12z" fill="${accent}" opacity=".24"></path>`,
|
||||
moon: `<path d="M23 6c-5 2-8 6-8 11 0 6 4 10 10 11-2 2-5 3-8 3-7 0-12-5-12-12S10 7 17 7c2 0 4 0 6-1z" fill="${accent}" opacity=".22"></path>`,
|
||||
traveler: `<path d="M10 27c3-4 13-4 16 0" fill="${shadow}" opacity=".42"></path><path d="M26 24l4 5" stroke="${accent}" stroke-width="2" stroke-linecap="round" opacity=".7"></path>`,
|
||||
}[shape] || "";
|
||||
return `<svg width="36" height="36" viewBox="0 0 36 36" aria-hidden="true" focusable="false">
|
||||
<rect x="2" y="2" width="32" height="32" rx="7" fill="#17120f"></rect>
|
||||
${motif}
|
||||
<path d="M10 18c0-7 4-11 9-11s9 4 9 11c0 8-4 12-9 12s-9-4-9-12z" fill="${accent}" opacity=".86"></path>
|
||||
<path d="M11 20c2 5 5 8 8 8s6-3 8-8" fill="${shadow}" opacity=".28"></path>
|
||||
<path d="${eye}" stroke="#1a120f" stroke-width="2.2" stroke-linecap="round"></path>
|
||||
<path d="${mouth}" stroke="#1a120f" stroke-width="2" stroke-linecap="round" fill="none"></path>
|
||||
<text x="28" y="31" text-anchor="middle" font-size="8" fill="#f8ead0" font-family="monospace">${symbol}</text>
|
||||
</svg>`;
|
||||
}
|
||||
function avatarHtml(character, size = "sm", expression = "soft", extraClass = "") {
|
||||
const config = characterConfig(character);
|
||||
const color = config.territoryColor || config.avatar?.accent || "#d3a64d";
|
||||
return `<span class="avatar avatar--${size} ${extraClass}" style="--avatar-color:${html(color)}" title="${html(config.displayLabel || character)} - ${html(config.avatar?.description || "observatory spirit")}">${avatarSvg(character, expression)}</span>`;
|
||||
}
|
||||
function avatarChip(character, entry = null) {
|
||||
const config = characterConfig(character);
|
||||
const expression = expressionFor(entry, character);
|
||||
return `<span class="character-chip" style="--avatar-color:${html(config.territoryColor || "#d3a64d")}">${avatarHtml(character, "xs", expression)}${html(config.displayLabel || character)}</span>`;
|
||||
}
|
||||
function primaryCharacter(entry) {
|
||||
return characterPresence(entry)[0]?.character || (entry?.characters || [])[0] || "z";
|
||||
}
|
||||
function characterPresence(entry) {
|
||||
const registry = state.meta.characterRegistry || {};
|
||||
const text = [entry?.title, entry?.content, entry?.triggerConditions, entry?.pageLocation, entry?.emotionalRole, entry?.mysteryLevel, ...(entry?.tags || []), ...(entry?.symbols || []), ...(entry?.narrativeArcs || [])].join(" ").toLowerCase();
|
||||
const scores = Object.keys(registry).map((character) => {
|
||||
const config = registry[character] || {};
|
||||
let score = (entry?.characters || []).includes(character) ? 10 : 0;
|
||||
(config.themes || []).forEach((theme) => { if (text.includes(String(theme).toLowerCase())) score += 2; });
|
||||
(config.motifs || []).forEach((motif) => { if (text.includes(String(motif).toLowerCase())) score += 3; });
|
||||
(config.aliases || []).forEach((alias) => { if (text.includes(String(alias).toLowerCase())) score += 2; });
|
||||
if (character === "lima" && /warm|home|safe|protect|kitchen|ring|love|rest/.test(text)) score += 3;
|
||||
if (character === "aphy" && /system|terminal|logic|error|debug|console|css|search/.test(text)) score += 3;
|
||||
if (character === "sensei chi" && /quiet|reflect|patience|tea|garden|lesson|wise/.test(text)) score += 3;
|
||||
if (character === "young z" && /play|child|crayon|sun|blanket|game|desk|nostalg/.test(text)) score += 3;
|
||||
if (character === "future z" && (/future|time|age 40|later|return|warning|ordinary/.test(text) || depth(entry || {}) === 4)) score += 3;
|
||||
if (character === "z" && /website|archive|observer|traveler|making|return/.test(text)) score += 2;
|
||||
return { character, score };
|
||||
}).filter((item) => item.score > 0).sort((a, b) => b.score - a.score);
|
||||
return scores.length ? scores : [{ character: "z", score: 1 }];
|
||||
}
|
||||
function svgAvatarMarker(character, entry, x, y, size = 28, className = "node-avatar") {
|
||||
const expression = expressionFor(entry, character);
|
||||
return `<g class="${className}" transform="translate(${x} ${y}) scale(${size / 36})">${avatarSvg(character, expression)}</g>`;
|
||||
}
|
||||
function splitList(value) {
|
||||
return String(value || "").split(",").map((item) => item.trim()).filter(Boolean);
|
||||
}
|
||||
@@ -3138,12 +3330,17 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
state.entries[index] = entryFromForm();
|
||||
state.selectedId = state.entries[index].id;
|
||||
rememberDraft();
|
||||
renderCharacterPreview(state.entries[index]);
|
||||
renderPreview(state.entries[index]);
|
||||
render();
|
||||
}
|
||||
function fillForm(entry) {
|
||||
if (!entry) return;
|
||||
$("drawerTitle").textContent = entry.title || "Untitled memory";
|
||||
$("drawerSub").textContent = `${layerName(depth(entry))} / ${entry.type || "memory"}`;
|
||||
const primary = primaryCharacter(entry);
|
||||
$("drawerAvatar").innerHTML = avatarHtml(primary, "lg", expressionFor(entry, primary), "avatar--breathing");
|
||||
$("drawerPresence").innerHTML = characterPresence(entry).slice(0, 4).map((item) => avatarChip(item.character, entry)).join("");
|
||||
$("title").value = entry.title || "";
|
||||
$("type").value = entry.type || "quote";
|
||||
$("familyLayer").value = String(depth(entry));
|
||||
@@ -3173,6 +3370,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
$("audioSettings").value = entry.audioSettings || "";
|
||||
$("animationTrigger").value = entry.animationTrigger || "";
|
||||
$("notes").value = entry.notes || "";
|
||||
renderCharacterPreview(entry);
|
||||
renderPreview(entry);
|
||||
renderConnections(entry);
|
||||
}
|
||||
@@ -3430,30 +3628,45 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
return `<line class="edge ${html(pair.kind)}" x1="${a.x}" y1="${a.y}" x2="${b.x}" y2="${b.y}"></line>`;
|
||||
}).join("");
|
||||
const occupied = [];
|
||||
graph.classList.toggle("zoom-0", state.zoom === 0);
|
||||
graph.classList.toggle("zoom-1", state.zoom === 1);
|
||||
graph.classList.toggle("zoom-2", state.zoom === 2);
|
||||
graph.classList.toggle("zoom-3", state.zoom === 3);
|
||||
const clusterNodes = clusters.map((cluster) => {
|
||||
const pos = state.clusterPositions.get(cluster.id);
|
||||
const color = state.mode === "character" ? (characterColor(cluster.label) || toneColors[cluster.tone] || "#d3a64d") : (toneColors[cluster.tone] || "#d3a64d");
|
||||
const anchorCharacter = state.mode === "character" ? cluster.label : cluster.character;
|
||||
const color = state.mode === "character" ? (characterColor(cluster.label) || toneColors[cluster.tone] || "#d3a64d") : (characterColor(cluster.character) || toneColors[cluster.tone] || "#d3a64d");
|
||||
const size = Math.min(120, 48 + Math.sqrt(cluster.entries.length) * 18 + cluster.rarity * 3);
|
||||
const label = labelForCluster(cluster, pos, size, occupied);
|
||||
const activeDrop = state.drag?.targetKind === "cluster" && state.drag.targetId === cluster.id;
|
||||
const grow = activeDrop ? 1.08 : 1;
|
||||
return `<g class="cluster-node${activeDrop ? " drop-target" : ""}" data-id="${html(cluster.id)}" tabindex="0" transform="translate(${pos.x} ${pos.y})">
|
||||
const avatar = anchorCharacter ? svgAvatarMarker(anchorCharacter, cluster.entries[0] || { characters: [anchorCharacter] }, -18, -18, state.mode === "character" ? 48 : 34, "cluster-avatar") : "";
|
||||
return `<g class="cluster-node${state.mode === "character" ? " territory-anchor" : ""}${activeDrop ? " drop-target" : ""}" data-id="${html(cluster.id)}" tabindex="0" transform="translate(${pos.x} ${pos.y})">
|
||||
<title>${html(`${anchorCharacter || cluster.label}: ${cluster.entries.length} memories`)}</title>
|
||||
<circle class="cluster-halo" r="${(size + 13) * grow}"></circle>
|
||||
<circle r="${size * grow}" fill="${color}" opacity="0.58"></circle>
|
||||
${avatar}
|
||||
<text class="count" x="0" y="7" text-anchor="middle">${cluster.entries.length}</text>
|
||||
</g>${label}`;
|
||||
}).join("");
|
||||
const nodes = entries.map((entry) => {
|
||||
const pos = state.positions.get(entry.id);
|
||||
if (!isWorldVisible(pos.x, pos.y, 220)) return "";
|
||||
const color = toneColors[entry.emotionalTone] || "#d3a64d";
|
||||
const character = primaryCharacter(entry);
|
||||
const characterColorValue = characterColor(character) || "#d3a64d";
|
||||
const color = toneColors[entry.emotionalTone] || characterColorValue;
|
||||
const size = 18 + Math.min(18, Number(entry.resonanceScore || 3) * 2.2) + (entry.rarity === "rare" || entry.rarity === "very rare" ? 8 : 0);
|
||||
const char = characterSymbol((entry.characters || [])[0]);
|
||||
const char = characterSymbol(character);
|
||||
const label = labelForEntry(entry, pos, size, occupied);
|
||||
const dragClass = dragClassForEntry(entry.id);
|
||||
return `<g class="memory-node${entry.id === state.selectedId ? " selected" : ""}${entry.enabled === false ? " resting" : ""}${dragClass}" data-id="${html(entry.id)}" tabindex="0" transform="translate(${pos.x} ${pos.y})">
|
||||
const avatar = svgAvatarMarker(character, entry, size * 0.42, -size - 16, Math.max(22, Math.min(32, size + 8)));
|
||||
const haloSize = size + Math.min(18, 7 + characterPresence(entry)[0].score);
|
||||
return `<g class="memory-node${entry.id === state.selectedId ? " selected" : ""}${entry.enabled === false ? " resting" : ""}${dragClass}" data-id="${html(entry.id)}" tabindex="0" transform="translate(${pos.x} ${pos.y})" style="--node-character-color:${html(characterColorValue)}">
|
||||
<title>${html(`${character} ${expressionFor(entry, character)}: ${entry.title || "memory"}`)}</title>
|
||||
<circle class="character-halo" r="${haloSize}"></circle>
|
||||
<circle r="${size}" fill="${color}" opacity="${entry.enabled === false ? 0.42 : 0.92}"></circle>
|
||||
<text x="0" y="4" text-anchor="middle">${html(char)}</text>
|
||||
${avatar}
|
||||
</g>${label}`;
|
||||
}).join("");
|
||||
graph.innerHTML = `<g id="world" transform="${cameraTransform()}">${rings}${clusterEdges}${edges}${clusterNodes}${nodes}<g id="dragLayer"></g></g>`;
|
||||
@@ -3529,15 +3742,17 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
if (state.camera.k < 0.16) return "";
|
||||
const scale = labelScale();
|
||||
const text = truncate(cluster.label, state.camera.k > 0.35 ? 46 : 28);
|
||||
const width = Math.max(180, text.length * 8 + 34);
|
||||
const width = Math.max(210, text.length * 8 + 68);
|
||||
const height = state.camera.k > 0.34 ? 68 : 40;
|
||||
const labelPos = { x: pos.x - width * scale / 2, y: pos.y + size + 28 };
|
||||
if (!reserveLabel(labelPos, width * scale, height * scale, occupied)) return "";
|
||||
const detail = state.camera.k > 0.34 ? `<text x="14" y="45" font-size="15">${cluster.entries.length} memories / ${html(layerName(cluster.depth))}</text>` : "";
|
||||
const avatar = cluster.character ? svgAvatarMarker(cluster.character, cluster.entries[0] || { characters: [cluster.character] }, 10, 8, 28, "cluster-label-avatar") : "";
|
||||
return `<g class="cluster-label" transform="translate(${labelPos.x} ${labelPos.y}) scale(${scale})">
|
||||
<rect width="${width}" height="${height}"></rect>
|
||||
<text x="14" y="25" font-size="20">${html(text)}</text>
|
||||
${detail}
|
||||
${avatar}
|
||||
<text x="${avatar ? 48 : 14}" y="25" font-size="20">${html(text)}</text>
|
||||
${detail.replace('x="14"', `x="${avatar ? 48 : 14}"`)}
|
||||
</g>`;
|
||||
}
|
||||
|
||||
@@ -3548,9 +3763,10 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
if (!isWorldVisible(pos.x, pos.y, 260)) return "";
|
||||
const scale = labelScale();
|
||||
const deep = state.camera.k > 1.25 || selected;
|
||||
const character = primaryCharacter(entry);
|
||||
const text = truncate(entry.title, deep ? 72 : state.camera.k > 0.85 ? 42 : 24);
|
||||
const meta = `${entry.type || "memory"} / ${entry.emotionalTone || "warm"}`;
|
||||
const width = Math.max(190, Math.min(420, text.length * 8 + 42));
|
||||
const meta = `${character} / ${entry.type || "memory"} / ${entry.emotionalTone || "warm"}`;
|
||||
const width = Math.max(220, Math.min(450, text.length * 8 + 76));
|
||||
const height = deep ? 92 : 44;
|
||||
const labelPos = { x: pos.x + size + 18, y: pos.y - height * scale / 2 };
|
||||
if (!selected && !reserveLabel(labelPos, width * scale, height * scale, occupied)) return "";
|
||||
@@ -3558,8 +3774,9 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
const metaLine = deep ? `<text class="node-meta" x="14" y="48" font-size="14">${html(meta)}</text>` : "";
|
||||
return `<g class="node-label${selected ? " selected" : ""}" transform="translate(${labelPos.x} ${labelPos.y}) scale(${scale})">
|
||||
<rect width="${width}" height="${height}"></rect>
|
||||
<text x="14" y="27" font-size="19">${html(text)}</text>
|
||||
${metaLine}${snippet}
|
||||
${svgAvatarMarker(character, entry, 10, 8, 28, "node-label-avatar")}
|
||||
<text x="48" y="27" font-size="19">${html(text)}</text>
|
||||
${metaLine.replace('x="14"', 'x="48"')}${snippet.replace('x="14"', 'x="48"')}
|
||||
</g>`;
|
||||
}
|
||||
|
||||
@@ -3585,7 +3802,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
function renderBreadcrumb(scope) {
|
||||
const filters = activeFilters();
|
||||
const parts = [`<button type="button" data-action="home">Observatory</button>`, `<button type="button" data-clear="mode">${html(state.mode)}</button>`];
|
||||
filters.forEach((filter) => parts.push(`<button type="button" title="${html(filter.help)}" data-clear="${html(filter.id)}">${html(filter.label)}</button>`));
|
||||
filters.forEach((filter) => parts.push(`<button type="button" title="${html(filter.help)}" data-clear="${html(filter.id)}">${filter.character ? avatarHtml(filter.character, "xs") : ""}${html(filter.label)}</button>`));
|
||||
if (scope.focusedCluster) parts.push(`<button type="button" data-clear="focus">${html(scope.focusedCluster.label)}</button>`);
|
||||
if (state.focusIds) parts.push(`<button type="button" data-clear="focus">${html(state.focusLabel || "Focused thread")}</button>`);
|
||||
$("breadcrumb").innerHTML = parts.join("");
|
||||
@@ -3598,7 +3815,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
const value = (id) => $(id).value;
|
||||
if (value("search")) filters.push({ id: "search", label: `Search: ${value("search")}`, help: "Only memories matching this text are visible." });
|
||||
if (value("layerFilter")) filters.push({ id: "layerFilter", label: `Depth: ${layerName(value("layerFilter"))}`, help: "Only this depth layer is visible." });
|
||||
if (value("characterFilter")) filters.push({ id: "characterFilter", label: `Character: ${value("characterFilter")}`, help: "Only memories in this character territory are visible." });
|
||||
if (value("characterFilter")) filters.push({ id: "characterFilter", label: `Character: ${value("characterFilter")}`, character: value("characterFilter"), help: "Only memories in this character territory are visible." });
|
||||
if (value("toneFilter")) filters.push({ id: "toneFilter", label: `Tone: ${value("toneFilter")}`, help: "Only this emotional tone is visible." });
|
||||
if (value("rarityFilter")) filters.push({ id: "rarityFilter", label: `Rarity: ${value("rarityFilter")}`, help: "Only this rarity is visible." });
|
||||
return filters;
|
||||
@@ -3614,7 +3831,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
$("activeState").innerHTML = `<span class="subtle">Whole sky visible. You can always return here.</span>`;
|
||||
return;
|
||||
}
|
||||
$("activeState").innerHTML = active.map((item) => `<span class="filter-pill" title="${html(item.help)}">${html(item.label)}<button type="button" aria-label="Remove ${html(item.label)}" data-clear="${html(item.id)}">x</button></span>`).join("") + `<button type="button" data-action="soft">Clear Focus</button><button type="button" data-action="full">Whole Sky</button>`;
|
||||
$("activeState").innerHTML = active.map((item) => `<span class="filter-pill" title="${html(item.help)}">${item.character ? avatarHtml(item.character, "xs") : ""}${html(item.label)}<button type="button" aria-label="Remove ${html(item.label)}" data-clear="${html(item.id)}">x</button></span>`).join("") + `<button type="button" data-action="soft">Clear Focus</button><button type="button" data-action="full">Whole Sky</button>`;
|
||||
$("activeState").querySelectorAll("[data-clear]").forEach((button) => button.addEventListener("click", () => clearExplorationPart(button.dataset.clear)));
|
||||
$("activeState").querySelector("[data-action='soft']")?.addEventListener("click", softReset);
|
||||
$("activeState").querySelector("[data-action='full']")?.addEventListener("click", fullReset);
|
||||
@@ -4081,14 +4298,18 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
}
|
||||
function renderPreview(entry) {
|
||||
const body = html(entry.content || "").replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
||||
$("preview").innerHTML = `${body || "<span class='subtle'>This memory is waiting for words.</span>"}<div class="pills" style="margin-top:10px"><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 memory is waiting for words.</span>"}<div class="pills" style="margin-top:10px">${characterPresence(entry).slice(0, 3).map((item) => avatarChip(item.character, entry)).join("")}<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>`;
|
||||
}
|
||||
function renderCharacterPreview(entry = null) {
|
||||
const draft = entry || { characters: normalizeCharacters($("characters").value), title: $("title").value, content: $("content").value, triggerConditions: $("triggerConditions").value, emotionalRole: $("emotionalRole").value, mysteryLevel: $("mysteryLevel").value, tags: splitList($("tags").value), symbols: splitList($("symbols").value), narrativeArcs: splitList($("narrativeArcs").value), pageLocation: $("pageLocation").value, familyLayer: $("familyLayer").value };
|
||||
$("characterPreview").innerHTML = characterPresence(draft).slice(0, 6).map((item) => avatarChip(item.character, draft)).join("") || "<span class='subtle'>z will keep this memory company until another character is chosen.</span>";
|
||||
}
|
||||
function renderConnections(entry) {
|
||||
const ids = new Set([...(entry.continuationLinks || []), ...(entry.echoes || []), ...(entry.thematicLinks || []), ...(entry.symbolicLinks || []), ...(entry.triggerLinks || []), ...(entry.parentLinks || []), ...(entry.childLinks || []), ...(entry.mirroredEntries || [])]);
|
||||
const sameCharacter = state.entries.filter((other) => other.id !== entry.id && (entry.characters || []).some((name) => (other.characters || []).includes(name))).slice(0, 4);
|
||||
const linked = [...ids].map((id) => state.entries.find((item) => item.id === id)).filter(Boolean);
|
||||
const items = [...linked, ...sameCharacter].slice(0, 10);
|
||||
$("connections").innerHTML = items.length ? items.map((item) => `<button class="connection" type="button" data-id="${html(item.id)}"><strong>${html(item.title)}</strong><br><span class="subtle">${html(layerName(depth(item)))} / ${html(item.type)}</span></button>`).join("") : "<div class='subtle'>No nearby memories yet. Drag from this node to another to begin a thread.</div>";
|
||||
$("connections").innerHTML = items.length ? items.map((item) => `<button class="connection" type="button" data-id="${html(item.id)}"><span class="guided-spirit">${avatarHtml(primaryCharacter(item), "xs", expressionFor(item, primaryCharacter(item)))}<strong>${html(item.title)}</strong></span><br><span class="subtle">${html(layerName(depth(item)))} / ${html(item.type)}</span></button>`).join("") : "<div class='subtle'>No nearby memories yet. Drag from this node to another to begin a thread.</div>";
|
||||
$("connections").querySelectorAll("button").forEach((button) => button.addEventListener("click", () => selectNode(button.dataset.id)));
|
||||
}
|
||||
function newEntry(type = "quote") {
|
||||
@@ -4163,6 +4384,11 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
$("toneFilter").innerHTML = opt("", "All tones") + state.meta.tones.map((value) => opt(value)).join("");
|
||||
$("rarityFilter").innerHTML = opt("", "All rarities") + state.meta.rarities.map((value) => opt(value)).join("");
|
||||
$("layerKey").innerHTML = (state.meta.layers || []).map((layer) => `<div class="layer-row"><span class="depth-mark">${html(layer.id)}</span><span><strong>${html(layer.name)}</strong><span class="subtle">${html(layer.meaning)}</span></span></div>`).join("");
|
||||
document.querySelectorAll("[data-guide-character]").forEach((item) => {
|
||||
if (item.querySelector(".avatar")) return;
|
||||
const character = item.dataset.guideCharacter;
|
||||
item.insertAdjacentHTML("afterbegin", avatarHtml(character, "xs", characterConfig(character).avatar?.mood || "soft"));
|
||||
});
|
||||
}
|
||||
async function save() {
|
||||
applyFormToState();
|
||||
|
||||
Reference in New Issue
Block a user