removing and clean
All checks were successful
Build Authoring Service / build (push) Successful in 9s

This commit is contained in:
2026-07-08 22:48:31 +01:00
parent 1528d245b4
commit d7d0054af4
4 changed files with 61 additions and 208 deletions

View File

@@ -27,45 +27,6 @@
const hour = now.getHours();
const path = window.location.pathname;
const state = readState();
const CHARACTER_AVATARS = {
"lima": {
avatar: "/assets/avatars/lima.jpg",
fallback: "/assets/avatars/z.jpeg",
aliases: ["lima"],
glow: "#f2c58b"
},
"aphy": {
avatar: "/assets/avatars/aphy.jpeg",
fallback: "/assets/avatars/z.jpeg",
aliases: ["aphy"],
glow: "#9dd3a6"
},
"sensei chi": {
avatar: "/assets/avatars/sensei-chi.jpeg",
fallback: "/assets/avatars/z.jpeg",
aliases: ["sensei chi", "sensei"],
glow: "#9ccddd"
},
"young z": {
avatar: "/assets/avatars/young-z.jpeg",
fallback: "/assets/avatars/z.jpeg",
aliases: ["young z", "young"],
glow: "#f0b85c"
},
"future z": {
avatar: "/assets/avatars/future-z.jpeg",
fallback: "/assets/avatars/z.jpeg",
aliases: ["future z", "future"],
glow: "#c2a4ee"
},
"z": {
avatar: "/assets/avatars/z.jpeg",
fallback: "/assets/avatars/z.jpeg",
aliases: ["z", "zaine"],
glow: "#ead68e"
}
};
// -----------------------------
// EDITABLE CONTENT
// -----------------------------
@@ -151,39 +112,13 @@
// UI HELPERS
// -----------------------------
function characterForMessage(message) {
const text = String(message || "").toLowerCase();
return Object.entries(CHARACTER_AVATARS).find(([, config]) => {
return (config.aliases || []).some((alias) => {
const escaped = alias.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
return new RegExp(`(^|[^a-z0-9-])${escaped}([^a-z0-9-]|$)`).test(text);
});
})?.[0] || "";
}
function avatarMarkup(character) {
const config = CHARACTER_AVATARS[character];
if (!config) return "";
return `<img class="hidden-message-avatar" src="${escapeHtml(config.avatar)}" alt="" loading="lazy" style="--hidden-avatar-glow:${escapeHtml(config.glow)}" onerror="this.onerror=null;this.src='${escapeHtml(config.fallback)}'">`;
}
function hiddenMessageMarkup(message) {
const character = characterForMessage(message);
return character
? `${avatarMarkup(character)}<span class="hidden-message-text">${escapeHtml(message)}</span>`
: `<span class="hidden-message-text">${escapeHtml(message)}</span>`;
return `<span class="hidden-message-text">${escapeHtml(message)}</span>`;
}
function setHiddenMessage(el, message) {
const character = characterForMessage(message);
el.classList.toggle("has-hidden-avatar", Boolean(character));
if (character) {
el.dataset.hiddenCharacter = character;
el.innerHTML = hiddenMessageMarkup(message);
} else {
delete el.dataset.hiddenCharacter;
el.textContent = message;
}
el.innerHTML = hiddenMessageMarkup(message);
}
function toast(message, duration) {
@@ -372,8 +307,7 @@
function showDriftNote(text, art) {
const panel = document.createElement("aside");
panel.className = "hidden-drift-note";
const character = characterForMessage(text);
panel.innerHTML = `<div class="hidden-drift-message${character ? " has-hidden-avatar" : ""}">${hiddenMessageMarkup(text)}</div><pre>${escapeHtml(art)}</pre><button type="button">Close</button>`;
panel.innerHTML = `<div class="hidden-drift-message">${hiddenMessageMarkup(text)}</div><pre>${escapeHtml(art)}</pre><button type="button">Close</button>`;
panel.querySelector("button").addEventListener("click", () => panel.remove());
document.body.appendChild(panel);
}
@@ -568,8 +502,7 @@
const visits = readState().layerVisits || {};
target.innerHTML = familyLayers.map((line, layer) => {
const count = visits[layer] || 0;
const character = characterForMessage(line);
return `<li class="${character ? "hidden-layer-message" : ""}"><strong>${hiddenMessageMarkup(line)}</strong><br><span>local encounters: ${count}</span></li>`;
return `<li class="hidden-layer-message"><strong>${hiddenMessageMarkup(line)}</strong><br><span>local encounters: ${count}</span></li>`;
}).join("");
}

View File

@@ -94,13 +94,6 @@ CHARACTER_REGISTRY = {
"aliases": ["Young Z", "young z", "young-z", "young_z", "young"],
"territoryColor": "#d8a95a",
"glow": "#f0b85c",
"avatar": "https://zainezq.com/assets/avatars/young-z.jpeg",
"fallbackAvatar": "https://zainezq.com/assets/avatars/z.jpeg",
"expressions": {
"default": "https://zainezq.com/assets/avatars/young-z.jpeg",
"nostalgic": "https://zainezq.com/assets/avatars/young-z.jpeg",
"playful": "https://zainezq.com/assets/avatars/young-z.jpeg",
},
"sizes": {"tiny": 18, "medium": 42, "close": 56, "focus": 82},
"observatory": {"territory": "nostalgic/playful memories", "shimmer": "soft"},
"presenceTones": ["nostalgic", "funny", "hopeful"],
@@ -116,31 +109,6 @@ CHARACTER_REGISTRY = {
"aliases": ["Z", "z", "zaine"],
"territoryColor": "#d6c38a",
"glow": "#ead68e",
"avatar": "https://zainezq.com/assets/avatars/z.jpeg",
"fallbackAvatar": "https://zainezq.com/assets/avatars/z.jpeg",
"expressions": {"default": "https://zainezq.com/assets/avatars/z.jpeg", "calm": "https://zainezq.com/assets/avatars/z.jpeg"},
"sizes": {"tiny": 18, "medium": 42, "close": 56, "focus": 82},
"observatory": {"territory": "self/archive memories", "shimmer": "steady"},
"presenceTones": ["warm", "soft", "hopeful"],
"presenceKeywords": ["website", "archive", "home", "self", "making", "return", "zaine"],
"symbol": "Z",
"motifs": ["archive", "website", "home"],
"themes": ["selfhood", "return", "making"],
"affinities": ["lima", "young z", "future z"],
},
"aphy": {
"id": "aphy",
"displayLabel": "aphy",
"aliases": ["Aphy", "aphy", "aphy_bot", "aphy bot"],
"territoryColor": "#7fb089",
"glow": "#9dd3a6",
"avatar": "https://zainezq.com/assets/avatars/aphy.jpeg",
"fallbackAvatar": "https://zainezq.com/assets/avatars/z.jpeg",
"expressions": {
"default": "https://zainezq.com/assets/avatars/aphy.jpeg",
"system": "https://zainezq.com/assets/avatars/aphy.jpeg",
"amused": "https://zainezq.com/assets/avatars/aphy.jpeg",
},
"sizes": {"tiny": 18, "medium": 42, "close": 56, "focus": 82},
"observatory": {"territory": "system/symbolic areas", "shimmer": "diagnostic"},
"presenceTones": ["funny", "strange", "hopeful"],
@@ -156,14 +124,6 @@ CHARACTER_REGISTRY = {
"aliases": ["Lima", "lima"],
"territoryColor": "#d06b78",
"glow": "#f2c58b",
"avatar": "https://zainezq.com/assets/avatars/lima.jpg",
"fallbackAvatar": "https://zainezq.com/assets/avatars/z.jpeg",
"expressions": {
"default": "https://zainezq.com/assets/avatars/lima.jpg",
"calm": "https://zainezq.com/assets/avatars/lima.jpg",
"warm": "https://zainezq.com/assets/avatars/lima.jpg",
"protective": "https://zainezq.com/assets/avatars/lima.jpg",
},
"sizes": {"tiny": 18, "medium": 42, "close": 56, "focus": 82},
"observatory": {"territory": "warm/protective arcs", "shimmer": "warm"},
"presenceTones": ["warm", "protective", "soft"],
@@ -179,13 +139,6 @@ CHARACTER_REGISTRY = {
"aliases": ["Sensei Chi", "sensei chi", "sensei-chi", "sensei_chi", "sensei"],
"territoryColor": "#75a9bd",
"glow": "#9ccddd",
"avatar": "https://zainezq.com/assets/avatars/sensei-chi.jpeg",
"fallbackAvatar": "https://zainezq.com/assets/avatars/z.jpeg",
"expressions": {
"default": "https://zainezq.com/assets/avatars/sensei-chi.jpeg",
"reflective": "https://zainezq.com/assets/avatars/sensei-chi.jpeg",
"wise": "https://zainezq.com/assets/avatars/sensei-chi.jpeg",
},
"sizes": {"tiny": 18, "medium": 42, "close": 56, "focus": 82},
"observatory": {"territory": "reflective areas", "shimmer": "quiet"},
"presenceTones": ["wise", "melancholy", "soft"],
@@ -201,13 +154,6 @@ CHARACTER_REGISTRY = {
"aliases": ["Future Z", "future z", "future-z", "future_z", "future"],
"territoryColor": "#a58ac9",
"glow": "#c2a4ee",
"avatar": "https://zainezq.com/assets/avatars/future-z.jpeg",
"fallbackAvatar": "https://zainezq.com/assets/avatars/z.jpeg",
"expressions": {
"default": "https://zainezq.com/assets/avatars/future-z.jpeg",
"temporal": "https://zainezq.com/assets/avatars/future-z.jpeg",
"reassuring": "https://zainezq.com/assets/avatars/future-z.jpeg",
},
"sizes": {"tiny": 18, "medium": 42, "close": 56, "focus": 82},
"observatory": {"territory": "temporal regions", "shimmer": "temporal"},
"presenceTones": ["hopeful", "melancholy", "wise"],

View File

@@ -1263,22 +1263,27 @@ HIDDEN_APP_HTML = r"""<!doctype html>
background: rgba(251, 241, 220, 0.08);
font-size: 12px;
}
.avatar-pill, .avatar-row, .character-tag {
.character-pill, .character-row, .character-tag {
display: inline-flex;
align-items: center;
gap: 7px;
}
.avatar-thumb {
width: var(--avatar-size, 28px);
height: var(--avatar-size, 28px);
flex: 0 0 var(--avatar-size, 28px);
.character-mark {
width: var(--character-size, 28px);
height: var(--character-size, 28px);
flex: 0 0 var(--character-size, 28px);
border-radius: 50%;
object-fit: cover;
border: 1px solid rgba(255, 246, 223, 0.72);
background: rgba(251, 241, 220, 0.08);
box-shadow: 0 0 18px color-mix(in srgb, var(--avatar-glow, #d3a64d) 42%, transparent);
box-shadow: 0 0 18px color-mix(in srgb, var(--character-glow, #d3a64d) 42%, transparent);
color: #fff4d7;
display: inline-grid;
font-size: calc(var(--character-size, 28px) * 0.46);
font-weight: 900;
line-height: 1;
place-items: center;
}
.avatar-row {
.character-row {
width: 100%;
justify-content: space-between;
margin-top: 10px;
@@ -1287,7 +1292,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
border-radius: 8px;
background: rgba(251, 241, 220, 0.055);
}
.avatar-row .subtle { font-size: 12px; }
.character-row .subtle { font-size: 12px; }
.character-tag {
min-height: 32px;
border: 1px solid rgba(232, 202, 139, 0.2);
@@ -1298,7 +1303,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
font-size: 12px;
font-weight: 800;
}
.character-tag .avatar-thumb { --avatar-size: 24px; }
.character-tag .character-mark { --character-size: 24px; }
.layer-key { display: grid; gap: 8px; margin-top: 14px; }
.layer-row {
display: grid;
@@ -1346,7 +1351,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
.memory-node.selected { opacity: 1; filter: drop-shadow(0 0 18px rgba(255, 231, 173, 0.45)); }
.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; }
.hit-target { fill: transparent; stroke: transparent; pointer-events: all; }
.memory-node .node-shell, .memory-node .node-portrait, .memory-node .node-marker, .memory-node .fragment-star {
.memory-node .node-shell, .memory-node .node-shell, .memory-node .node-marker, .memory-node .fragment-star {
stroke: rgba(255, 246, 223, 0.82);
stroke-width: 1.4;
filter: drop-shadow(0 0 7px var(--node-glow, rgba(255, 232, 177, 0.18)));
@@ -1354,16 +1359,16 @@ HIDDEN_APP_HTML = r"""<!doctype html>
}
.memory-node .fragment-star { stroke-width: 1.1; }
.memory-node .fragment-ring { fill: none; stroke: rgba(255, 246, 223, 0.24); stroke-width: 1; stroke-dasharray: 2 7; }
.memory-node .node-breathe, .cluster-node .node-breathe { animation: avatar-breathe 8s ease-in-out infinite; transform-box: fill-box; transform-origin: center; }
.memory-node .node-breathe, .cluster-node .node-breathe { animation: character-breathe 8s ease-in-out infinite; transform-box: fill-box; transform-origin: center; }
.sequence-badge { fill: rgba(18, 16, 13, 0.88); stroke: rgba(255, 243, 199, 0.68); stroke-width: 1; }
.sequence-number { fill: #ffe7ad; font-size: 10px; font-weight: 850; paint-order: stroke; stroke: rgba(14, 11, 8, 0.72); stroke-width: 2px; }
.selection-halo { fill: rgba(255, 231, 173, 0.1); stroke: rgba(255, 243, 199, 0.95); stroke-width: 3; stroke-dasharray: 4 9; filter: drop-shadow(0 0 22px rgba(255, 231, 173, 0.62)); animation: selection-pulse 2.8s ease-in-out infinite; pointer-events: none; }
.memory-node:hover .node-shell, .memory-node:hover .node-portrait, .memory-node:hover .fragment-star { filter: drop-shadow(0 0 20px var(--node-glow, rgba(255, 232, 177, 0.4))); }
.memory-node.selected .node-shell, .memory-node.selected .node-portrait, .memory-node.selected .fragment-star { stroke: #fff3c7; stroke-width: 3; }
.memory-node.dragging .node-shell, .memory-node.dragging .node-portrait { stroke: #f3cd7a; stroke-width: 3; opacity: 0.72; }
.memory-node.compatible .node-shell, .memory-node.compatible .node-portrait { stroke: rgba(255, 243, 199, 0.92); stroke-width: 2.4; filter: drop-shadow(0 0 16px rgba(255, 232, 177, 0.46)); }
.memory-node.drop-target .node-shell, .memory-node.drop-target .node-portrait { stroke: #fff5c9; stroke-width: 4; filter: drop-shadow(0 0 24px rgba(255, 232, 177, 0.7)); }
.memory-node.invalid-drop .node-shell, .memory-node.invalid-drop .node-portrait { stroke: rgba(208, 107, 120, 0.9); stroke-width: 3; }
.memory-node:hover .node-shell, .memory-node:hover .node-shell, .memory-node:hover .fragment-star { filter: drop-shadow(0 0 20px var(--node-glow, rgba(255, 232, 177, 0.4))); }
.memory-node.selected .node-shell, .memory-node.selected .node-shell, .memory-node.selected .fragment-star { stroke: #fff3c7; stroke-width: 3; }
.memory-node.dragging .node-shell, .memory-node.dragging .node-shell { stroke: #f3cd7a; stroke-width: 3; opacity: 0.72; }
.memory-node.compatible .node-shell, .memory-node.compatible .node-shell { stroke: rgba(255, 243, 199, 0.92); stroke-width: 2.4; filter: drop-shadow(0 0 16px rgba(255, 232, 177, 0.46)); }
.memory-node.drop-target .node-shell, .memory-node.drop-target .node-shell { stroke: #fff5c9; stroke-width: 4; filter: drop-shadow(0 0 24px rgba(255, 232, 177, 0.7)); }
.memory-node.invalid-drop .node-shell, .memory-node.invalid-drop .node-shell { stroke: rgba(208, 107, 120, 0.9); stroke-width: 3; }
.cluster-node { cursor: pointer; transition: transform 160ms ease, filter 160ms ease; }
.cluster-node circle {
stroke: rgba(255, 243, 199, 0.75);
@@ -1671,7 +1676,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
0%, 100% { opacity: 0.72; }
50% { opacity: 1; }
}
@keyframes avatar-breathe {
@keyframes character-breathe {
0%, 100% { transform: scale(1); opacity: 0.92; }
50% { transform: scale(1.035); opacity: 1; }
}
@@ -1890,7 +1895,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
<section class="panel">
<h2 id="drawerTitle">Choose a memory</h2>
<div id="drawerSub" class="subtle">The side panel opens from the constellation.</div>
<div id="drawerAvatars" class="pills" style="margin-top:10px"></div>
<div id="drawerCharacters" class="pills" style="margin-top:10px"></div>
<div id="preview" class="preview" style="margin-top:10px"></div>
</section>
<section class="panel editor-panel story-editor-panel" id="storyEditorPanel">
@@ -2017,7 +2022,6 @@ HIDDEN_APP_HTML = r"""<!doctype html>
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" };
const defaultAvatarPath = "/assets/avatars/z.jpeg";
const appearanceLabels = {
observatory: "Observatory stars",
tooltip: "Soft tooltips",
@@ -2062,30 +2066,25 @@ HIDDEN_APP_HTML = r"""<!doctype html>
function entryById(id) {
return state.entries.find((entry) => entry.id === id);
}
function avatarRegistry() {
function characterRegistry() {
return state.meta.characterRegistry || {};
}
function characterConfig(character) {
const registry = avatarRegistry();
const registry = characterRegistry();
return registry[character] || registry.z || {};
}
function characterLabel(character) {
return characterConfig(character).displayLabel || character || "z";
}
function avatarPath(character, expression = "default") {
const config = characterConfig(character);
const expressions = config.expressions || {};
return expressions[expression] || expressions.default || config.avatar || config.fallbackAvatar || defaultAvatarPath;
}
function avatarGlow(character) {
function characterGlow(character) {
const config = characterConfig(character);
return config.glow || config.territoryColor || "#d3a64d";
}
function avatarSize(character, size = "medium") {
function characterSize(character, size = "medium") {
const sizes = characterConfig(character).sizes || {};
return Number(sizes[size] || sizes.medium || 42);
}
function avatarExpressionForEntry(entry) {
function characterExpressionForEntry(entry) {
const tone = entry?.emotionalTone || "";
if (tone === "protective") return "protective";
if (tone === "wise") return "wise";
@@ -2095,10 +2094,10 @@ HIDDEN_APP_HTML = r"""<!doctype html>
if (String(entry?.type || "").includes("future")) return "temporal";
return "default";
}
function avatarImg(character, size = "medium", expression = "default", className = "") {
const px = avatarSize(character, size);
function characterImg(character, size = "medium", expression = "default", className = "") {
const px = characterSize(character, size);
const label = characterLabel(character);
return `<img class="avatar-thumb ${html(className)}" src="${html(avatarPath(character, expression))}" alt="${html(label)} portrait" loading="lazy" style="--avatar-size:${px}px;--avatar-glow:${html(avatarGlow(character))}" onerror="this.onerror=null;this.src='${html(characterConfig(character).fallbackAvatar || defaultAvatarPath)}'">`;
return `<span class="character-mark ${html(className)}" title="${html(label)}" aria-label="${html(label)}" style="--character-size:${px}px;--character-glow:${html(characterGlow(character))}">${html(characterSymbol(character))}</span>`;
}
function entryTextCorpus(entry) {
return [
@@ -2127,7 +2126,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
return explicit + tone + keywords + motifs;
}
function primaryCharacterForEntry(entry) {
const candidates = Object.keys(avatarRegistry());
const candidates = Object.keys(characterRegistry());
const scored = candidates.map((character) => ({ character, score: characterPresenceScore(entry, character) })).sort((a, b) => b.score - a.score);
return scored[0]?.score > 0 ? scored[0].character : (entry.characters || [])[0] || "z";
}
@@ -2136,13 +2135,11 @@ HIDDEN_APP_HTML = r"""<!doctype html>
const primary = primaryCharacterForEntry(entry);
return [primary, ...listed.filter((character) => character !== primary)].filter(Boolean);
}
function svgAvatar(character, radius, clipId, expression = "default", extraClass = "") {
const diameter = radius * 2;
function svgCharacter(character, radius, clipId, expression = "default", extraClass = "") {
const fallback = characterSymbol(character);
return `<circle class="node-shell ${html(extraClass)}" r="${radius}" fill="${html(characterColor(character) || avatarGlow(character))}" opacity="0.22"></circle>
<image class="node-portrait ${html(extraClass)}" href="${html(avatarPath(character, expression))}" x="${-radius}" y="${-radius}" width="${diameter}" height="${diameter}" preserveAspectRatio="xMidYMid slice" clip-path="url(#${html(clipId)})"></image>
return `<circle class="node-shell ${html(extraClass)}" r="${radius}" fill="${html(characterColor(character) || characterGlow(character))}" opacity="0.22"></circle>
<circle class="node-shell ${html(extraClass)}" r="${radius}" fill="none"></circle>
<text class="node-fallback" x="0" y="4" text-anchor="middle" opacity="0.32">${html(fallback)}</text>`;
<text class="node-fallback ${html(extraClass)}" x="0" y="4" text-anchor="middle" opacity="0.74">${html(fallback)}</text>`;
}
function normalizeCharacterName(value) {
const raw = String(value || "").trim();
@@ -2511,11 +2508,11 @@ HIDDEN_APP_HTML = r"""<!doctype html>
}
function renderCharacterPresence(entry) {
const characters = charactersForEntry(entry);
$("drawerAvatars").innerHTML = characters.map((character, index) => `<span class="avatar-pill pill" style="--avatar-glow:${html(avatarGlow(character))}">${avatarImg(character, index === 0 ? "medium" : "tiny", avatarExpressionForEntry(entry))}${html(characterLabel(character))}</span>`).join("");
$("characterTags").innerHTML = characters.map((character) => `<span class="character-tag" style="--avatar-glow:${html(avatarGlow(character))}">${avatarImg(character, "tiny", avatarExpressionForEntry(entry))}${html(characterLabel(character))}</span>`).join("");
$("drawerCharacters").innerHTML = characters.map((character, index) => `<span class="character-pill pill" style="--character-glow:${html(characterGlow(character))}">${characterImg(character, index === 0 ? "medium" : "tiny", characterExpressionForEntry(entry))}${html(characterLabel(character))}</span>`).join("");
$("characterTags").innerHTML = characters.map((character) => `<span class="character-tag" style="--character-glow:${html(characterGlow(character))}">${characterImg(character, "tiny", characterExpressionForEntry(entry))}${html(characterLabel(character))}</span>`).join("");
const primary = primaryCharacterForEntry(entry);
const territory = characterConfig(primary).observatory?.territory || "this memory territory";
$("presencePreview").innerHTML = `<span class="avatar-row" style="--avatar-glow:${html(avatarGlow(primary))}"><span>${avatarImg(primary, "medium", avatarExpressionForEntry(entry))}</span><span><strong>${html(characterLabel(primary))}</strong><br><span class="subtle">presence preview: ${html(territory)}</span></span></span>`;
$("presencePreview").innerHTML = `<span class="character-row" style="--character-glow:${html(characterGlow(primary))}"><span>${characterImg(primary, "medium", characterExpressionForEntry(entry))}</span><span><strong>${html(characterLabel(primary))}</strong><br><span class="subtle">presence preview: ${html(territory)}</span></span></span>`;
}
function layerName(layer) {
return (state.meta.layers || []).find((item) => item.id === String(layer))?.name || `Layer ${layer}`;
@@ -2867,13 +2864,13 @@ HIDDEN_APP_HTML = r"""<!doctype html>
const label = labelForCluster(cluster, pos, size, occupied);
const activeDrop = state.drag?.targetKind === "cluster" && state.drag.targetId === cluster.id;
const grow = activeDrop ? 1.08 : 1;
const portraitRadius = Math.max(28, size * 0.46) * grow;
const symbolRadius = Math.max(28, size * 0.46) * grow;
const clipId = `clip-cluster-${slugify(cluster.id)}`;
defs.push(`<clipPath id="${html(clipId)}"><circle r="${portraitRadius}"></circle></clipPath>`);
return `<g class="cluster-node${activeDrop ? " drop-target" : ""}" data-id="${html(cluster.id)}" tabindex="0" transform="translate(${pos.x} ${pos.y})" style="--node-glow:${html(avatarGlow(character))}">
defs.push(`<clipPath id="${html(clipId)}"><circle r="${symbolRadius}"></circle></clipPath>`);
return `<g class="cluster-node${activeDrop ? " drop-target" : ""}" data-id="${html(cluster.id)}" tabindex="0" transform="translate(${pos.x} ${pos.y})" style="--node-glow:${html(characterGlow(character))}">
<circle class="cluster-halo" r="${(size + 13) * grow}"></circle>
<circle r="${size * grow}" fill="${color}" opacity="0.58"></circle>
${state.zoom > 0 || state.mode === "character" ? svgAvatar(character, portraitRadius, clipId, "default", "node-breathe") : ""}
${state.zoom > 0 || state.mode === "character" ? svgCharacter(character, symbolRadius, clipId, "default", "node-breathe") : ""}
<text class="count" x="0" y="7" text-anchor="middle">${cluster.entries.length}</text>
</g>${label}`;
}).join("");
@@ -2884,7 +2881,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
const color = characterColor(character) || toneColors[entry.emotionalTone] || "#d3a64d";
const isFragment = (entry.contentClass || "fragment") === "fragment" || (entry.observatoryRole || "") === "ambient";
const baseSize = (isFragment ? 10 : 18) + Math.min(isFragment ? 10 : 18, Number(entry.resonanceScore || 3) * (isFragment ? 1.6 : 2.2)) + (entry.rarity === "rare" || entry.rarity === "very rare" ? 8 : 0);
const zoomSize = state.zoom === 0 ? avatarSize(character, "tiny") : state.zoom === 1 ? avatarSize(character, "medium") : state.zoom === 2 ? avatarSize(character, "close") : avatarSize(character, "focus");
const zoomSize = state.zoom === 0 ? characterSize(character, "tiny") : state.zoom === 1 ? characterSize(character, "medium") : state.zoom === 2 ? characterSize(character, "close") : characterSize(character, "focus");
const size = isFragment ? baseSize : Math.max(baseSize, zoomSize / 2);
const label = labelForEntry(entry, pos, size, occupied);
const dragClass = dragClassForEntry(entry.id);
@@ -2899,14 +2896,14 @@ HIDDEN_APP_HTML = r"""<!doctype html>
const sequenceBadge = sequenceNumber && (state.camera.k > 0.26 || activeStoryIds.has(entry.id) || related)
? `<g transform="translate(${Math.max(12, size * 0.62)} ${-Math.max(12, size * 0.62)})"><circle class="sequence-badge" r="${badgeRadius}"></circle><text class="sequence-number" x="0" y="3.5" text-anchor="middle">${sequenceNumber}</text></g>`
: "";
return `<g class="memory-node${isSelected ? " selected" : ""}${related && !isSelected ? " related-node" : ""}${dimmed ? " dimmed" : ""}${entry.enabled === false ? " resting" : ""}${dragClass}" data-id="${html(entry.id)}" tabindex="0" transform="translate(${pos.x} ${pos.y})" style="--node-glow:${html(avatarGlow(character))}">
return `<g class="memory-node${isSelected ? " selected" : ""}${related && !isSelected ? " related-node" : ""}${dimmed ? " dimmed" : ""}${entry.enabled === false ? " resting" : ""}${dragClass}" data-id="${html(entry.id)}" tabindex="0" transform="translate(${pos.x} ${pos.y})" style="--node-glow:${html(characterGlow(character))}">
<circle class="hit-target" r="${Math.max(34, size + 16)}"></circle>
${isSelected ? `<circle class="selection-halo" r="${Math.max(38, size + 18)}"></circle>` : ""}
${isFragment
? `<circle class="fragment-ring constellation-shimmer" r="${size + 9}" opacity="${entry.enabled === false ? 0.22 : 0.62}"></circle><circle class="fragment-star" r="${size}" fill="${color}" opacity="${entry.enabled === false ? 0.34 : 0.84}"></circle><text x="0" y="4" text-anchor="middle">${html(characterSymbol(character))}</text>`
: distant
? `<circle class="node-marker constellation-shimmer" r="${size}" fill="${color}" opacity="${entry.enabled === false ? 0.32 : 0.78}"></circle><text x="0" y="4" text-anchor="middle">${html(characterSymbol(character))}</text>`
: svgAvatar(character, size, clipId, avatarExpressionForEntry(entry), state.zoom >= 2 ? "node-breathe" : "")}
: svgCharacter(character, size, clipId, characterExpressionForEntry(entry), state.zoom >= 2 ? "node-breathe" : "")}
${sequenceBadge}
</g>${label}`;
}).join("");
@@ -3055,7 +3052,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)}">${filter.character ? avatarImg(filter.character, "tiny") : ""}${html(filter.label)}</button>`));
filters.forEach((filter) => parts.push(`<button type="button" title="${html(filter.help)}" data-clear="${html(filter.id)}">${filter.character ? characterImg(filter.character, "tiny") : ""}${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("");
@@ -3085,7 +3082,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)}">${item.character ? avatarImg(item.character, "tiny") : ""}${html(item.label)}${item.id === "selection" ? "" : `<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 ? characterImg(item.character, "tiny") : ""}${html(item.label)}${item.id === "selection" ? "" : `<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);
@@ -3554,8 +3551,8 @@ 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 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>`;
const characterPills = charactersForEntry(entry).map((character) => `<span class="character-pill pill" style="--character-glow:${html(characterGlow(character))}">${characterImg(character, "tiny", characterExpressionForEntry(entry))}${html(characterLabel(character))}</span>`).join("");
$("preview").innerHTML = `${body || "<span class='subtle'>This entry is waiting for words.</span>"}<div class="pills" style="margin-top:10px">${characterPills}<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);
@@ -3582,7 +3579,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
$("storyFlow").innerHTML = "";
return;
}
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="character-pill pill">${characterImg(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><div class="actions" style="margin-top:10px"><button type="button" data-read-story="${html(story.id)}">Read this story</button></div>`;
$("storyCover").querySelector("[data-read-story]")?.addEventListener("click", () => openStoryReader(story.id));
const items = storyItems(story);
@@ -3631,7 +3628,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
const pool = state.entries.filter((entry) => !inStory.has(entry.id) && (!query || entryTextCorpus(entry).includes(query))).slice(0, 24);
$("memoryPool").innerHTML = pool.map((entry) => {
const character = primaryCharacterForEntry(entry);
return `<button class="connection avatar-row" type="button" draggable="true" data-id="${html(entry.id)}" style="--avatar-glow:${html(avatarGlow(character))}"><span>${avatarImg(character, "tiny", avatarExpressionForEntry(entry))}</span><span><strong>${html(entry.title)}</strong><br><span class="subtle">${html(entry.emotionalTone || "warm")} / ${html(entry.type || "memory")}</span></span></button>`;
return `<button class="connection character-row" type="button" draggable="true" data-id="${html(entry.id)}" style="--character-glow:${html(characterGlow(character))}"><span>${characterImg(character, "tiny", characterExpressionForEntry(entry))}</span><span><strong>${html(entry.title)}</strong><br><span class="subtle">${html(entry.emotionalTone || "warm")} / ${html(entry.type || "memory")}</span></span></button>`;
}).join("") || "<div class='subtle'>Every visible memory is already in this story.</div>";
$("memoryPool").querySelectorAll("[data-id]").forEach((button) => {
button.addEventListener("click", () => selectNode(button.dataset.id));
@@ -3672,7 +3669,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
$("readerStage").innerHTML = `<article class="reader-scene reader-section"><div class="reader-kicker">Story scene</div><h2>${html(item.title || "Story section")}</h2><p class="reader-copy">${html(item.content || "This scene is waiting for words.")}</p></article>`;
} else {
const entry = entryById(item.id);
const characters = charactersForEntry(entry).map((character) => `<span class="avatar-pill pill">${avatarImg(character, "tiny", avatarExpressionForEntry(entry))}${html(characterLabel(character))}</span>`).join("");
const characters = charactersForEntry(entry).map((character) => `<span class="character-pill pill">${characterImg(character, "tiny", characterExpressionForEntry(entry))}${html(characterLabel(character))}</span>`).join("");
const kind = (entry.contentClass || "fragment") === "fragment" ? "Reusable memory light" : "Reusable memory";
$("readerStage").innerHTML = `<article class="reader-scene reader-memory"><div class="reader-kicker">${html(kind)}</div><h2>${html(entry.title || "Untitled memory")}</h2><p class="reader-copy">${html(entry.content || "This memory is waiting for words.")}</p><div class="pills" style="justify-content:center;margin-top:18px">${characters}<span class="pill">${html(entry.emotionalTone || "warm")}</span><span class="pill">appears in ${storiesForEntry(entry.id).length} ${storiesForEntry(entry.id).length === 1 ? "story" : "stories"}</span></div></article>`;
}
@@ -3916,10 +3913,10 @@ 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("");
$("characterAtlas").innerHTML = Object.keys(avatarRegistry()).map((character) => {
$("characterAtlas").innerHTML = Object.keys(characterRegistry()).map((character) => {
const config = characterConfig(character);
const territory = config.observatory?.territory || "memory territory";
return `<button class="connection avatar-row" type="button" data-character="${html(character)}" style="--avatar-glow:${html(avatarGlow(character))}"><span>${avatarImg(character, "medium")}</span><span><strong>${html(characterLabel(character))}</strong><br><span class="subtle">${html(territory)}</span></span></button>`;
return `<button class="connection character-row" type="button" data-character="${html(character)}" style="--character-glow:${html(characterGlow(character))}"><span>${characterImg(character, "medium")}</span><span><strong>${html(characterLabel(character))}</strong><br><span class="subtle">${html(territory)}</span></span></button>`;
}).join("");
$("characterAtlas").querySelectorAll("[data-character]").forEach((button) => button.addEventListener("click", () => {
snapshotExploration(`character territory: ${button.dataset.character}`);

View File

@@ -3,14 +3,12 @@
from __future__ import annotations
import json
import mimetypes
import os
import sys
import time
from email.utils import formatdate
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Any
from urllib.parse import parse_qs, urlparse
@@ -89,27 +87,6 @@ class Handler(BaseHTTPRequestHandler):
except Exception as exc:
self.send_json({"error": str(exc)}, HTTPStatus.INTERNAL_SERVER_ERROR)
return
if parsed.path.startswith("/assets/avatars/"):
try:
relative = Path(parsed.path.lstrip("/"))
if relative.is_absolute() or ".." in relative.parts:
self.send_error(HTTPStatus.NOT_FOUND)
return
asset_path = (ROOT / relative).resolve()
avatars_root = (ROOT / "assets" / "avatars").resolve()
if not asset_path.is_file() or avatars_root not in asset_path.parents:
self.send_error(HTTPStatus.NOT_FOUND)
return
body = asset_path.read_bytes()
self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", mimetypes.guess_type(asset_path.name)[0] or "application/octet-stream")
self.send_header("Content-Length", str(len(body)))
self.send_header("Cache-Control", "no-store")
self.end_headers()
self.wfile.write(body)
except Exception:
self.send_error(HTTPStatus.NOT_FOUND)
return
self.send_error(HTTPStatus.NOT_FOUND)
def do_POST(self) -> None: