feat: session modes (Talk/Cash) + live session HUD
Lyra now switches register based on what she's doing at the table instead of
being a wishy-washy companion mid-session.
Modes (lyra/modes.py):
- Talk (default companion) + Cash (live cash copilot); a mode = prompt card +
tool allow-list. Tool gating via tools.specs(allow=).
- Two-register Cash voice: act-first one-line logging when fed facts; full warm
companion voice for strategy / tilt / mental game.
- mode persisted per chat session (new sessions.mode column); auto-switch into
Cash when start_session fires; UI forces cloud backend in Cash (tools only
fire there).
Stack tracking + HUD:
- log_stack tool + poker_stack_log table; live net while sitting (stack - buy-in).
- poker.hud() bundle; /session HUD page (stack sparkline, hands, villains, notes,
stats) polling /session/data every 5s; Talk/Cash switcher + Session nav.
Endpoints: /session, /session/data, GET/POST /sessions/{id}/mode, /modes.
tests/test_modes.py (gating, mode roundtrip, stack/HUD); 36 tests green. v0.3.0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+71
-13
@@ -25,8 +25,8 @@
|
||||
<div class="mobile-menu-section">
|
||||
<h4>Mode</h4>
|
||||
<select id="mobileMode">
|
||||
<option value="standard">Standard</option>
|
||||
<option value="cortex">Cortex</option>
|
||||
<option value="conversation">💬 Talk</option>
|
||||
<option value="poker_cash">♠ Cash</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
|
||||
<div class="mobile-menu-section">
|
||||
<h4>Actions</h4>
|
||||
<button id="mobileSessionBtn">🎬 Session HUD</button>
|
||||
<button id="mobileThinkingStreamBtn">📜 Live Log (inline)</button>
|
||||
<button id="mobileFullLogBtn">⛶ Full Log</button>
|
||||
<button id="mobileJournalBtn">📔 Journal</button>
|
||||
@@ -59,10 +60,11 @@
|
||||
</button>
|
||||
<span class="brand">Lyra</span>
|
||||
<span class="brand-dot" id="brandDot" title="Relay status"></span>
|
||||
<button class="mode-badge" id="modeBadge" type="button" title="Tap to toggle Talk / Cash mode">💬 Talk</button>
|
||||
<label for="mode">Mode:</label>
|
||||
<select id="mode">
|
||||
<option value="standard">Standard</option>
|
||||
<option value="cortex">Cortex</option>
|
||||
<option value="conversation">💬 Talk</option>
|
||||
<option value="poker_cash">♠ Cash</option>
|
||||
</select>
|
||||
<button id="settingsBtn" style="margin-left: auto;">⚙ Settings</button>
|
||||
<div id="theme-toggle">
|
||||
@@ -78,6 +80,7 @@
|
||||
<button id="renameSessionBtn">✏️ Rename</button>
|
||||
<button id="thinkingStreamBtn" title="Show live activity log">📜 Live Log</button>
|
||||
<a id="fullLogBtn" href="/logs" target="_blank" rel="noopener" title="Open the full-page log" role="button">⛶ Full Log</a>
|
||||
<a id="sessionBtn" href="/session" target="_blank" rel="noopener" title="Live session HUD" role="button">🎬 Session</a>
|
||||
<a id="mindBtn" href="/self" target="_blank" rel="noopener" title="Read her mind — Lyra's current self-state" role="button">🧠 Mind</a>
|
||||
<a id="handsBtn" href="/hands" target="_blank" rel="noopener" title="Recorded poker hands" role="button">🃏 Hands</a>
|
||||
</div>
|
||||
@@ -118,6 +121,7 @@
|
||||
<!-- Bottom tab bar (mobile only; hides while the keyboard is open) -->
|
||||
<nav id="tabbar" aria-label="Primary navigation">
|
||||
<a class="tab active" href="/" aria-current="page"><span class="ti">💬</span><span class="tl">Chat</span></a>
|
||||
<a class="tab" href="/session"><span class="ti">🎬</span><span class="tl">Session</span></a>
|
||||
<a class="tab" href="/hands"><span class="ti">🃏</span><span class="tl">Hands</span></a>
|
||||
<a class="tab" href="/self"><span class="ti">🧠</span><span class="tl">Mind</span></a>
|
||||
<button class="tab" id="moreTab" type="button"><span class="ti">⋯</span><span class="tl">More</span></button>
|
||||
@@ -298,6 +302,10 @@
|
||||
// Which chat backend to use (local Ollama vs cloud OpenAI).
|
||||
let backend = localStorage.getItem("standardModeBackend") || "local";
|
||||
|
||||
// Cash mode is useless without tools, and tools only fire on cloud — so a
|
||||
// live poker session forces the cloud backend regardless of the saved pick.
|
||||
if (mode === "poker_cash") backend = "cloud";
|
||||
|
||||
const body = {
|
||||
mode: mode,
|
||||
messages: history,
|
||||
@@ -376,6 +384,12 @@
|
||||
finalizeAssistantBubble(div, full || "(no reply)");
|
||||
history.push({ role: "assistant", content: full || "(no reply)" });
|
||||
await saveSession();
|
||||
|
||||
// If she opened a session this turn, the server auto-flips to Cash mode —
|
||||
// reflect that here so the badge/HUD follow without a manual switch.
|
||||
if (document.getElementById("mode").value !== "poker_cash") {
|
||||
loadModeFor(currentSession);
|
||||
}
|
||||
}
|
||||
|
||||
function createAssistantBubble() {
|
||||
@@ -499,6 +513,44 @@
|
||||
}
|
||||
|
||||
|
||||
// ----- Conversation mode (Talk / Cash) -----
|
||||
const MODE_LABELS = { conversation: "💬 Talk", poker_cash: "♠ Cash" };
|
||||
|
||||
// Reflect a mode value across the controls + header accent (no network call).
|
||||
function applyMode(value) {
|
||||
if (!MODE_LABELS[value]) value = "conversation";
|
||||
const desk = document.getElementById("mode");
|
||||
const mob = document.getElementById("mobileMode");
|
||||
const badge = document.getElementById("modeBadge");
|
||||
if (desk) desk.value = value;
|
||||
if (mob) mob.value = value;
|
||||
if (badge) badge.textContent = MODE_LABELS[value];
|
||||
document.body.classList.toggle("cash-mode", value === "poker_cash");
|
||||
localStorage.setItem("lyraMode", value);
|
||||
}
|
||||
|
||||
// User picked a mode: apply locally + persist it to this session on the server.
|
||||
async function chooseMode(value) {
|
||||
applyMode(value);
|
||||
if (!currentSession) return;
|
||||
try {
|
||||
await fetch(`${RELAY_BASE}/sessions/${currentSession}/mode`, {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ mode: value })
|
||||
});
|
||||
} catch (e) { /* non-fatal: the mode still rides along in the chat body */ }
|
||||
}
|
||||
|
||||
// Pull the active mode for a session from the server (fallback: last local choice).
|
||||
async function loadModeFor(sessionId) {
|
||||
let value = localStorage.getItem("lyraMode") || "conversation";
|
||||
try {
|
||||
const r = await fetch(`${RELAY_BASE}/sessions/${sessionId}/mode`);
|
||||
if (r.ok) { const d = await r.json(); if (d.mode) value = d.mode; }
|
||||
} catch (e) { /* keep the local fallback */ }
|
||||
applyMode(value);
|
||||
}
|
||||
|
||||
async function checkHealth() {
|
||||
try {
|
||||
const resp = await fetch(API_URL.replace("/v1/chat/completions", "/_health"));
|
||||
@@ -578,19 +630,20 @@
|
||||
mobileMenuOverlay.addEventListener("click", closeMobileMenu);
|
||||
document.getElementById("moreTab").addEventListener("click", toggleMobileMenu);
|
||||
|
||||
// Sync mobile menu controls with desktop
|
||||
// Mode controls (Talk / Cash): the desktop select, the mobile-menu select,
|
||||
// and the always-visible header badge all funnel through chooseMode.
|
||||
const mobileMode = document.getElementById("mobileMode");
|
||||
const desktopMode = document.getElementById("mode");
|
||||
const modeBadge = document.getElementById("modeBadge");
|
||||
|
||||
// Sync mode selection
|
||||
mobileMode.addEventListener("change", (e) => {
|
||||
desktopMode.value = e.target.value;
|
||||
desktopMode.dispatchEvent(new Event("change"));
|
||||
});
|
||||
desktopMode.addEventListener("change", (e) => chooseMode(e.target.value));
|
||||
mobileMode.addEventListener("change", (e) => { closeMobileMenu(); chooseMode(e.target.value); });
|
||||
modeBadge.addEventListener("click", () =>
|
||||
chooseMode(desktopMode.value === "poker_cash" ? "conversation" : "poker_cash"));
|
||||
|
||||
desktopMode.addEventListener("change", (e) => {
|
||||
mobileMode.value = e.target.value;
|
||||
});
|
||||
// Reflect the last-used mode immediately; the per-session value loads once
|
||||
// the current session is known (below).
|
||||
applyMode(localStorage.getItem("lyraMode") || "conversation");
|
||||
|
||||
// Mobile theme toggle
|
||||
document.getElementById("mobileToggleThemeBtn").addEventListener("click", () => {
|
||||
@@ -700,6 +753,7 @@
|
||||
// Load current session history
|
||||
if (currentSession) {
|
||||
await loadSession(currentSession);
|
||||
await loadModeFor(currentSession);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -710,6 +764,7 @@
|
||||
localStorage.setItem("currentSession", currentSession);
|
||||
addMessage("system", `Switched to session: ${getSessionName(currentSession)}`);
|
||||
await loadSession(currentSession);
|
||||
await loadModeFor(currentSession);
|
||||
});
|
||||
|
||||
// Create new session
|
||||
@@ -1023,6 +1078,9 @@
|
||||
document.getElementById("mobileJournalBtn").addEventListener("click", () => {
|
||||
closeMobileMenu(); window.location.href = "/journal";
|
||||
});
|
||||
document.getElementById("mobileSessionBtn").addEventListener("click", () => {
|
||||
closeMobileMenu(); window.location.href = "/session";
|
||||
});
|
||||
|
||||
// Connect to the global live log on page load.
|
||||
connectThinkingStream();
|
||||
|
||||
Reference in New Issue
Block a user