@@ -0,0 +1,172 @@
<!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 : 1 px solid var ( - - border ) ; padding : env ( safe - area - inset - top ) 14 px 0 ;
}
. topbar { display : flex ; align-items : center ; gap : 10 px ; padding : 13 px 0 12 px ; }
. topbar h1 { font-size : 1.05 rem ; margin : 0 ; font-weight : 600 ; }
. topbar a . back { color : var ( - - accent ) ; text-decoration : none ; font-size : .95 rem ; }
. updated { margin-left : auto ; color : var ( - - fade ) ; font-size : .78 rem ; }
. dot { width : 9 px ; height : 9 px ; border-radius : 50 % ; background : var ( - - good ) ; box-shadow : 0 0 8 px var ( - - good ) ; flex : none ; opacity : .35 ; transition : opacity .2 s ; }
. dot . pulse { opacity : 1 ; }
main { max-width : 680 px ; margin : 0 auto ; padding : 16 px 14 px 40 px ; }
. card { background : var ( - - bg - elev ) ; border : 1 px solid var ( - - border ) ; border-radius : 14 px ; padding : 16 px ; margin-bottom : 14 px ; }
. label { color : var ( - - fade ) ; font-size : .72 rem ; text-transform : uppercase ; letter-spacing : .6 px ; margin : 0 0 10 px ; }
. mood-row { display : flex ; align-items : baseline ; gap : 12 px ; flex-wrap : wrap ; }
. mood { font-size : 2.1 rem ; font-weight : 700 ; letter-spacing : .2 px ; }
. mood-sub { color : var ( - - fade ) ; font-size : .9 rem ; }
. meter { margin : 11 px 0 ; }
. meter-top { display : flex ; justify-content : space-between ; font-size : .85 rem ; margin-bottom : 5 px ; }
. meter-top . v { color : var ( - - fade ) ; font-variant-numeric : tabular-nums ; }
. track { height : 8 px ; background : var ( - - bg - line ) ; border-radius : 999 px ; overflow : hidden ; }
. fill { height : 100 % ; border-radius : 999 px ; transition : width .5 s ease ; }
. prose { font-size : 1.02 rem ; 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 : 10 px 0 10 px 18 px ; border-bottom : 1 px solid var ( - - bg - line ) ;
font-size : .98 rem ; line-height : 1.5 ;
}
ul . reflections li : last-child { border-bottom : none ; }
ul . reflections li :: before { content : "› " ; position : absolute ; left : 2 px ; color : var ( - - violet ) ; font-weight : 700 ; }
. foot { display : flex ; flex-wrap : wrap ; gap : 14 px ; color : var ( - - fade ) ; font-size : .82 rem ; padding : 4 px 2 px ; }
. foot b { color : var ( - - text ) ; font-weight : 600 ; }
. err { color : var ( - - low ) ; text-align : center ; padding : 30 px ; }
< / 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 (don\\' t lose 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 & 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 >