7b65f81d7e
Completes the poker copilot loop: talk through a session -> structured capture
-> generated writeup in Brian's format, remembered + exportable.
- poker.generate_recap(): LLM produces Brian's .md log (Session Header, Money
Flow, Overview, Timeline, Key Hands w/ assessments, Villain Notes, Confidence
Bank, Scar Notes, Mental Game, Final Assessment) from the session's structured
data + the linked chat conversation; stored on poker_sessions.recap_md
- sessions now capture chat_session_id (via tool ctx) to pull the right convo;
list_recent_hands() for browsing
- generate_recap tool ("write up the recap")
- web: /recap/{id} (renders the md) + /recap/{id}/download (.md attachment) +
/hands browser (recent hands -> /hand/{id}); nav links added (desktop + mobile)
- tests: recap generation (stubbed), recent-hands listing
Verified live: recap for the Meadows session rendered + downloaded; all pages 200.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
79 lines
3.8 KiB
HTML
79 lines
3.8 KiB
HTML
<!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 — Recap</title>
|
|
<style>
|
|
:root{--bg:#0b0d12;--bg-elev:#141821;--bg-line:#11141b;--border:#232936;--text:#e6e9ef;--fade:#8b93a7;--accent:#7aa2ff;}
|
|
*{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:12px 0;flex-wrap:wrap;}
|
|
.topbar h1{font-size:1.02rem;margin:0;font-weight:600;}
|
|
.topbar a.back{color:var(--accent);text-decoration:none;font-size:.92rem;}
|
|
.dl{margin-left:auto;background:#1b2333;border:1px solid var(--border);color:var(--accent);
|
|
border-radius:8px;padding:7px 12px;font-size:.85rem;text-decoration:none;}
|
|
main{max-width:740px;margin:0 auto;padding:18px 16px 48px;line-height:1.6;}
|
|
h1,h2,h3,h4{line-height:1.3;color:var(--text);}
|
|
main>h1:first-child{margin-top:0;}
|
|
h2{font-size:1.18rem;border-bottom:1px solid var(--border);padding-bottom:5px;margin-top:26px;color:var(--accent);}
|
|
h3{font-size:1.04rem;margin-top:18px;}
|
|
ul{padding-left:22px;} li{margin:3px 0;}
|
|
strong{color:var(--text);} hr{border:none;border-top:1px solid var(--border);margin:20px 0;}
|
|
code{background:rgba(255,255,255,.08);padding:1px 5px;border-radius:4px;font-size:.9em;}
|
|
.err{color:var(--fade);text-align:center;padding:46px 16px;}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<div class="topbar">
|
|
<h1>📋 Recap</h1>
|
|
<a class="back" href="/">← Chat</a>
|
|
<a class="back" href="/hands">Hands</a>
|
|
<a class="dl" id="dl">⬇ .md</a>
|
|
</div>
|
|
</header>
|
|
<main id="root"><p class="err">Loading recap…</p></main>
|
|
|
|
<script>
|
|
const bt = String.fromCharCode(96);
|
|
function esc(s){return String(s==null?'':s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");}
|
|
function inline(s){
|
|
const codeRe = new RegExp(bt+"([^"+bt+"]+)"+bt,"g");
|
|
return esc(s).replace(codeRe,"<code>$1</code>")
|
|
.replace(/\*\*([^*]+)\*\*/g,"<strong>$1</strong>")
|
|
.replace(/(^|[^*])\*([^*\n]+)\*/g,"$1<em>$2</em>");
|
|
}
|
|
function md(src){
|
|
const lines=String(src||"").replace(/\r\n/g,"\n").split("\n");
|
|
const out=[]; let list=null;
|
|
const flush=()=>{if(list){out.push("<ul>"+list.map(i=>"<li>"+inline(i)+"</li>").join("")+"</ul>");list=null;}};
|
|
for(const raw of lines){
|
|
const t=raw.replace(/\s+$/,""); let m;
|
|
if(!t.trim()){flush();continue;}
|
|
if(/^(-{3,}|\*{3,}|_{3,})$/.test(t.trim())){flush();out.push("<hr>");continue;}
|
|
if((m=t.match(/^(#{1,6})\s+(.*)$/))){flush();const n=m[1].length;out.push(`<h${n}>${inline(m[2])}</h${n}>`);continue;}
|
|
if((m=t.match(/^\s*[-*+]\s+(.*)$/))){(list=list||[]).push(m[1]);continue;}
|
|
flush();out.push("<p>"+inline(t)+"</p>");
|
|
}
|
|
flush(); return out.join("\n");
|
|
}
|
|
async function load(){
|
|
const id=location.pathname.split('/')[2];
|
|
document.getElementById('dl').href=`/recap/${id}/download`;
|
|
try{
|
|
const r=await fetch(`/recap/${id}/data`,{cache:'no-store'});
|
|
const d=await r.json();
|
|
if(!d.markdown){document.getElementById('root').innerHTML='<p class="err">No recap yet for this session. Ask Lyra to write one ("generate the recap").</p>';return;}
|
|
document.getElementById('root').innerHTML=md(d.markdown);
|
|
}catch(e){document.getElementById('root').innerHTML='<p class="err">Couldn\'t load the recap.</p>';}
|
|
}
|
|
load();
|
|
</script>
|
|
</body>
|
|
</html>
|