Files
project-lyra/lyra/web/static/self.html
T
serversdown 4c8f7202da feat: make the two-step reflection observable (draft -> revised -> critique)
You couldn't see her actually correct herself — /self showed only the result.
Now:
- reflect() logs the draft, the revised/committed version, and the self-critique
  to the live log as an expandable "view details" block
- POST /self/reflect runs a reflection in the web process so it lands in /logs
  live (reflections normally run in the dream process, whose logs only go to
  journald); "↻ Reflect now" button on /self triggers it, with a logs ↗ link
- log viewers relabel the expander "view full prompt" -> "view details" (it now
  carries prompts and reflection diffs)

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

199 lines
8.6 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; }
#reflectBtn {
background: #1b2333; border: 1px solid var(--border); color: var(--accent);
border-radius: 8px; padding: 6px 11px; font-size: .82rem; cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
#reflectBtn:disabled { opacity: .5; cursor: default; }
.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>
<a class="back" href="/logs" target="_blank" rel="noopener" title="Watch the live log">logs ↗</a>
<button id="reflectBtn" title="Make her reflect now (draft → self-critique → revise). Watch it in /logs.">↻ Reflect now</button>
<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();
const meta = (s.metacognition || []).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="card">
<p class="label">How she's caught herself thinking</p>
${meta.length
? `<ul class="reflections">${meta.map(m => `<li>${esc(m)}</li>`).join('')}</ul>`
: `<p class="prose" style="color:var(--fade)">Nothing flagged yet — she examines each reflection for drift and flattery, and notes what she catches here.</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>';
}
}
const reflectBtn = document.getElementById('reflectBtn');
reflectBtn.addEventListener('click', async () => {
reflectBtn.disabled = true;
const old = reflectBtn.textContent;
reflectBtn.textContent = '… thinking';
try { await fetch('/self/reflect', { method: 'POST' }); await refresh(); }
catch (e) { /* ignore */ }
finally { reflectBtn.disabled = false; reflectBtn.textContent = old; }
});
refresh();
setInterval(refresh, 12000);
document.addEventListener('visibilitychange', () => { if (!document.hidden) refresh(); });
</script>
</body>
</html>