feat: poker phase 2 — session recap (.md) generation, export, hands browser
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>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
<!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 — Hands</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:13px 0;}
|
||||
.topbar h1{font-size:1.05rem;margin:0;font-weight:600;}
|
||||
.topbar a.back{color:var(--accent);text-decoration:none;font-size:.92rem;}
|
||||
.count{margin-left:auto;color:var(--fade);font-size:.8rem;}
|
||||
main{max-width:640px;margin:0 auto;padding:12px 12px 40px;}
|
||||
a.hand{display:flex;align-items:center;gap:12px;text-decoration:none;color:var(--text);
|
||||
background:var(--bg-elev);border:1px solid var(--border);border-radius:10px;padding:10px 12px;margin-bottom:8px;}
|
||||
a.hand:active{background:#1b2333;}
|
||||
.cards{display:flex;gap:4px;flex:none;}
|
||||
.card{display:inline-flex;flex-direction:column;align-items:center;justify-content:center;
|
||||
width:24px;height:33px;background:#f4f4f0;color:#111;border-radius:4px;font-weight:700;font-size:.72rem;line-height:1;}
|
||||
.card.red{color:#c8102e;} .card.unknown{background:#2a3550;color:#7c879e;}
|
||||
.card .nosuit{color:#9aa3b5;}
|
||||
.mid{flex:1;min-width:0;}
|
||||
.ln1{font-size:.92rem;}
|
||||
.ln2{font-size:.74rem;color:var(--fade);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
||||
.res{flex:none;font-variant-numeric:tabular-nums;font-weight:600;}
|
||||
.pos-res{color:#5ad1a0;} .neg-res{color:#ff6b6b;}
|
||||
.tag{font-size:.62rem;text-transform:uppercase;letter-spacing:.4px;color:var(--accent);}
|
||||
.empty{color:var(--fade);text-align:center;padding:46px 16px;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="topbar">
|
||||
<h1>🃏 Hands</h1>
|
||||
<a class="back" href="/">← Chat</a>
|
||||
<span class="count" id="count"></span>
|
||||
</div>
|
||||
</header>
|
||||
<main id="root"><p class="empty">Loading…</p></main>
|
||||
|
||||
<script>
|
||||
const SUIT={s:"♠",h:"♥",d:"♦",c:"♣"}, RED=new Set(["h","d"]);
|
||||
function esc(s){const d=document.createElement('div');d.textContent=s==null?'':String(s);return d.innerHTML;}
|
||||
function cardEl(code){
|
||||
if(!code) return '';
|
||||
const c=String(code).trim();
|
||||
if(c.toLowerCase()==='x') return '<span class="card unknown">?</span>';
|
||||
const m=c.match(/^(10|[2-9TJQKA])\s*([shdcx])$/i);
|
||||
if(!m) return `<span class="card">${esc(c)}</span>`;
|
||||
const r=m[1].toUpperCase().replace('10','T'), s=m[2].toLowerCase();
|
||||
if(s==='x') return `<span class="card"><span>${r}</span><span class="nosuit">·</span></span>`;
|
||||
return `<span class="card${RED.has(s)?' red':''}"><span>${r}</span><span>${SUIT[s]}</span></span>`;
|
||||
}
|
||||
const cards=str=>(str?String(str).trim().split(/\s+/):[]).map(cardEl).join('');
|
||||
|
||||
async function load(){
|
||||
try{
|
||||
const r=await fetch('/hands/data',{cache:'no-store'});
|
||||
const hands=(await r.json()).hands||[];
|
||||
document.getElementById('count').textContent=`${hands.length} hand${hands.length===1?'':'s'}`;
|
||||
if(!hands.length){document.getElementById('root').innerHTML='<p class="empty">No hands recorded yet. Tell Lyra: "log this hand: …"</p>';return;}
|
||||
document.getElementById('root').innerHTML=hands.map(h=>{
|
||||
const res=h.result!=null?`<span class="res ${h.result>=0?'pos-res':'neg-res'}">${h.result>=0?'+':''}${h.result}</span>`:'';
|
||||
const meta=[h.stakes,h.venue,(h.at||'').slice(0,10)].filter(Boolean).join(' · ');
|
||||
const tag=h.tag?` · <span class="tag">${esc(h.tag)}</span>`:'';
|
||||
return `<a class="hand" href="/hand/${h.id}">
|
||||
<span class="cards">${cards(h.hole_cards)||'<span class="card unknown">?</span>'}</span>
|
||||
<span class="mid">
|
||||
<div class="ln1">${esc(h.position||'')} ${h.board?'· '+'<span class="cards" style="display:inline-flex">'+cards(h.board)+'</span>':''}</div>
|
||||
<div class="ln2">${esc(meta)}${tag}</div>
|
||||
</span>${res}</a>`;
|
||||
}).join('');
|
||||
}catch(e){document.getElementById('root').innerHTML='<p class="empty">Couldn\'t load hands.</p>';}
|
||||
}
|
||||
load();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user