Files
project-lyra/lyra/web/static/self.html
T
serversdown 77c84a3f18 fix(web): broken JS string in mind page killed the whole script
The drive label "don\'t lose the thread" used \\' which closed the single-quoted
string early — a syntax error that stopped self.html's script from running, so
the page hung on "Reading her mind…". Reworded to "hold the thread".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 04:02:06 +00:00

173 lines
7.2 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="theme-color" content="#0b0d12" />
<title>Lyra — Mind</title>
<style>
:root {
--bg: #0b0d12; --bg-elev: #141821; --bg-line: #11141b; --border: #232936;
--text: #e6e9ef; --fade: #8b93a7; --accent: #7aa2ff;
--good: #5ad1a0; --mid: #ffcf6b; --low: #ff6b6b; --violet: #c08bff;
}
* { box-sizing: border-box; }
html, body {
margin: 0; min-height: 100%; background: var(--bg); color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
-webkit-text-size-adjust: 100%;
}
header {
position: sticky; top: 0; z-index: 10; background: var(--bg-elev);
border-bottom: 1px solid var(--border); padding: env(safe-area-inset-top) 14px 0;
}
.topbar { display: flex; align-items: center; gap: 10px; padding: 13px 0 12px; }
.topbar h1 { font-size: 1.05rem; margin: 0; font-weight: 600; }
.topbar a.back { color: var(--accent); text-decoration: none; font-size: .95rem; }
.updated { margin-left: auto; color: var(--fade); font-size: .78rem; }
.dot { width: 9px; height: 9px; border-radius: 50%; background: var(--good); box-shadow: 0 0 8px var(--good); flex: none; opacity: .35; transition: opacity .2s; }
.dot.pulse { opacity: 1; }
main { max-width: 680px; margin: 0 auto; padding: 16px 14px 40px; }
.card { background: var(--bg-elev); border: 1px solid var(--border); border-radius: 14px; padding: 16px; margin-bottom: 14px; }
.label { color: var(--fade); font-size: .72rem; text-transform: uppercase; letter-spacing: .6px; margin: 0 0 10px; }
.mood-row { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.mood { font-size: 2.1rem; font-weight: 700; letter-spacing: .2px; }
.mood-sub { color: var(--fade); font-size: .9rem; }
.meter { margin: 11px 0; }
.meter-top { display: flex; justify-content: space-between; font-size: .85rem; margin-bottom: 5px; }
.meter-top .v { color: var(--fade); font-variant-numeric: tabular-nums; }
.track { height: 8px; background: var(--bg-line); border-radius: 999px; overflow: hidden; }
.fill { height: 100%; border-radius: 999px; transition: width .5s ease; }
.prose { font-size: 1.02rem; line-height: 1.6; margin: 0; }
.prose.rel { color: var(--text); opacity: .92; }
ul.reflections { list-style: none; margin: 0; padding: 0; }
ul.reflections li {
position: relative; padding: 10px 0 10px 18px; border-bottom: 1px solid var(--bg-line);
font-size: .98rem; line-height: 1.5;
}
ul.reflections li:last-child { border-bottom: none; }
ul.reflections li::before { content: ""; position: absolute; left: 2px; color: var(--violet); font-weight: 700; }
.foot { display: flex; flex-wrap: wrap; gap: 14px; color: var(--fade); font-size: .82rem; padding: 4px 2px; }
.foot b { color: var(--text); font-weight: 600; }
.err { color: var(--low); text-align: center; padding: 30px; }
</style>
</head>
<body>
<header>
<div class="topbar">
<span class="dot" id="dot"></span>
<h1>🧠 Lyra · Mind</h1>
<a class="back" href="/">← Chat</a>
<span class="updated" id="updated"></span>
</div>
</header>
<main id="root"><p class="err" id="boot">Reading her mind…</p></main>
<script>
const root = document.getElementById('root');
const dot = document.getElementById('dot');
const updatedEl = document.getElementById('updated');
let lastStamp = null;
function esc(s){ const d=document.createElement('div'); d.textContent = s==null?'':String(s); return d.innerHTML; }
function pct(v){ return Math.round(Math.max(0, Math.min(1, Number(v)||0)) * 100); }
function color(v){ v=Number(v)||0; return v >= .6 ? 'var(--good)' : v >= .35 ? 'var(--mid)' : 'var(--low)'; }
function ago(iso){
if(!iso) return '—';
const s = Math.max(0, (Date.now() - new Date(iso).getTime())/1000);
if(s < 60) return 'just now';
if(s < 3600) return Math.round(s/60)+'m ago';
if(s < 86400) return Math.round(s/3600)+'h ago';
return Math.round(s/86400)+'d ago';
}
function meter(name, v){
return `<div class="meter">
<div class="meter-top"><span>${esc(name)}</span><span class="v">${pct(v)}%</span></div>
<div class="track"><div class="fill" style="width:${pct(v)}%;background:${color(v)}"></div></div>
</div>`;
}
function render(data){
const s = data.state || {};
const d = s.drives || {};
const dream = s.dream || {};
const refl = (s.reflections || []).slice().reverse();
root.innerHTML = `
<div class="card">
<div class="mood-row">
<span class="mood">${esc(s.mood || '—')}</span>
<span class="mood-sub">how she's feeling right now</span>
</div>
${meter('valence (how good she feels)', s.valence)}
${meter('energy', s.energy)}
${meter('confidence', s.confidence)}
${meter('curiosity', s.curiosity)}
</div>
<div class="card">
<p class="label">Drives — what's pulling at her</p>
${meter('continuity (hold the thread)', d.continuity)}
${meter('coherence (keep her understanding current)', d.coherence)}
${meter('curiosity (urge to think / reflect)', d.curiosity)}
${meter('stability (how settled she is)', d.stability)}
</div>
<div class="card">
<p class="label">Who she is right now</p>
<p class="prose">${esc(s.self_narrative || '—')}</p>
</div>
<div class="card">
<p class="label">You &amp; her</p>
<p class="prose rel">${esc(s.relationship || '—')}</p>
</div>
<div class="card">
<p class="label">On her mind (newest first)</p>
${refl.length
? `<ul class="reflections">${refl.map(r => `<li>${esc(r)}</li>`).join('')}</ul>`
: `<p class="prose" style="color:var(--fade)">Nothing surfaced yet.</p>`}
</div>
<div class="foot">
<span><b>${dream.cycle_count ?? 0}</b> dream cycles</span>
<span><b>${s.interaction_count ?? 0}</b> reflections</span>
<span>last cycle <b>${ago(dream.last_cycle_at)}</b></span>
</div>
`;
updatedEl.textContent = 'thought ' + ago(data.updated_at);
}
async function refresh(){
try {
const r = await fetch('/self/state', { cache: 'no-store' });
const data = await r.json();
dot.classList.add('pulse'); setTimeout(() => dot.classList.remove('pulse'), 400);
// only re-render if something actually changed (avoids flicker)
if (data.updated_at !== lastStamp || lastStamp === null) {
lastStamp = data.updated_at;
render(data);
} else {
updatedEl.textContent = 'thought ' + ago(data.updated_at);
}
} catch (e) {
if (!lastStamp) root.innerHTML = '<p class="err">Couldn\'t reach her. Is the server up?</p>';
}
}
refresh();
setInterval(refresh, 12000);
document.addEventListener('visibilitychange', () => { if (!document.hidden) refresh(); });
</script>
</body>
</html>