Task #23: Workshop session mode UI — fund once, ask many (all review issues fixed)
## Changes ### the-matrix/js/session.js (new module) - Full session lifecycle: create → invoice → deposit poll → active → request → topup → restore - Presets + number input for deposit (200–10,000 sats) and topup amounts; reads from input on submit - Input validation: 200–10,000 sats range enforced in JS before API call - Auto-closes panel after deposit payment confirms (closePanel in _startDepositPolling success branch) - Low-balance condition fixed: `isSessionActive()` (covers both 'active' and 'paused') not just `active` - HUD: updates `#session-hud-balance` span with "Balance: X sats"; `#session-hud-topup` link clickable - Topup reads from `#session-topup-input` number field, same validation - localStorage restore: validates session via GET, restores macaroon + balance + UI state on reload - Expired/401 sessions: clears storage, resets all UI - Request in-flight guard prevents double-submit; send button disabled during request ### the-matrix/js/ui.js - `setSessionSendHandler(fn)` — override input bar submit when session active - `setInputBarSessionMode(active, placeholder)` — green border + placeholder swap - `send()` routes to session handler when set, falls back to WS visitor_message ### the-matrix/index.html - `#top-buttons` flex container: "⚡ SUBMIT JOB" (blue) + "⚡ FUND SESSION" (teal) side-by-side - `#session-hud` with `#session-hud-balance` span + `#session-hud-topup` link (pointer-events: all) - `#session-panel` (left slide-in): fund / invoice / active / topup steps - Fund + topup steps each have preset buttons AND a number input (200–10k range) - Added 10k preset button to both step grids - `#visitor-input.session-active` green pulse border animation (3s keyframe) - `#low-balance-notice` strip above input bar with inline Top Up button - CSS: `.session-amount-input` green styled, spin buttons hidden; `.session-amount-row` flex layout - CSS: `.primary-green` / `.muted` panel button variants for session panel theme ### the-matrix/js/main.js - Import + call `initSessionPanel()` in firstInit block ## Verification - npm run build: clean (0 errors, 15 modules) - Testkit: 27/27 PASS (session tests 11–16, 22 all green)
This commit is contained in:
@@ -42,7 +42,15 @@
|
||||
color: #22aa66;
|
||||
text-shadow: 0 0 6px #11663388;
|
||||
letter-spacing: 1px;
|
||||
pointer-events: all;
|
||||
line-height: 1.9;
|
||||
}
|
||||
#session-hud-topup {
|
||||
color: #22aa66; text-decoration: none; margin-left: 5px;
|
||||
letter-spacing: 1px; text-shadow: 0 0 6px #11663388;
|
||||
cursor: pointer;
|
||||
}
|
||||
#session-hud-topup:hover { color: #44dd88; text-decoration: underline; }
|
||||
|
||||
#connection-status {
|
||||
position: fixed; top: 16px; right: 16px;
|
||||
@@ -242,6 +250,26 @@
|
||||
.session-amount-btn:hover { background: #0e2318; border-color: #22aa66; color: #44dd88; }
|
||||
.session-amount-btn.active { background: #0e2318; border-color: #22aa66; color: #44dd88; }
|
||||
|
||||
/* Amount number inputs */
|
||||
.session-amount-input {
|
||||
background: #020806; border: 1px solid #0e2318;
|
||||
color: #44dd88; font-family: 'Courier New', monospace;
|
||||
font-size: 15px; font-weight: bold; letter-spacing: 1px;
|
||||
padding: 6px 10px; width: 110px;
|
||||
outline: none; border-radius: 2px;
|
||||
transition: border-color 0.2s;
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
.session-amount-input:focus { border-color: #22aa66; }
|
||||
.session-amount-input::-webkit-outer-spin-button,
|
||||
.session-amount-input::-webkit-inner-spin-button { -webkit-appearance: none; }
|
||||
.session-amount-row {
|
||||
display: flex; align-items: center; gap: 6px; margin: 8px 0;
|
||||
}
|
||||
.session-amount-row span {
|
||||
color: #226644; font-size: 11px; letter-spacing: 1px;
|
||||
}
|
||||
|
||||
/* Active session balance tag */
|
||||
.amount-tag.session-green { border-color: #22aa66; color: #44dd88; }
|
||||
|
||||
@@ -353,7 +381,10 @@
|
||||
<h1>THE WORKSHOP</h1>
|
||||
<div id="fps">FPS: --</div>
|
||||
<div id="active-jobs">JOBS: 0</div>
|
||||
<div id="session-hud"></div>
|
||||
<div id="session-hud">
|
||||
<span id="session-hud-balance">Balance: -- sats</span>
|
||||
<a href="#" id="session-hud-topup">⚡ Top Up</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="connection-status">OFFLINE</div>
|
||||
@@ -430,16 +461,21 @@
|
||||
|
||||
<!-- Step: fund — choose deposit amount -->
|
||||
<div data-session-step="fund">
|
||||
<div class="panel-label">DEPOSIT AMOUNT</div>
|
||||
<div class="panel-label">DEPOSIT AMOUNT (200–10,000 sats)</div>
|
||||
<div class="session-amount-presets">
|
||||
<button class="session-amount-btn" data-sats="200">200</button>
|
||||
<button class="session-amount-btn active" data-sats="500">500</button>
|
||||
<button class="session-amount-btn" data-sats="1000">1000</button>
|
||||
<button class="session-amount-btn" data-sats="2000">2000</button>
|
||||
<button class="session-amount-btn" data-sats="5000">5000</button>
|
||||
<button class="session-amount-btn" data-sats="10000">10k</button>
|
||||
</div>
|
||||
<div class="amount-tag" id="session-amount-display">500 sats</div>
|
||||
<button class="panel-btn primary-green" id="session-create-btn" style="margin-top:16px">START SESSION →</button>
|
||||
<div class="session-amount-row">
|
||||
<input type="number" id="session-amount-input" class="session-amount-input"
|
||||
min="200" max="10000" value="500" step="1" />
|
||||
<span>sats</span>
|
||||
</div>
|
||||
<button class="panel-btn primary-green" id="session-create-btn" style="margin-top:12px">START SESSION →</button>
|
||||
<div id="session-status-fund"></div>
|
||||
</div>
|
||||
|
||||
@@ -470,16 +506,21 @@
|
||||
|
||||
<!-- Step: topup — choose topup amount and pay -->
|
||||
<div data-session-step="topup" style="display:none">
|
||||
<div class="panel-label">TOPUP AMOUNT</div>
|
||||
<div class="panel-label">TOPUP AMOUNT (200–10,000 sats)</div>
|
||||
<div class="session-amount-presets">
|
||||
<button class="session-amount-btn" data-sats="200">200</button>
|
||||
<button class="session-amount-btn active" data-sats="500">500</button>
|
||||
<button class="session-amount-btn" data-sats="1000">1000</button>
|
||||
<button class="session-amount-btn" data-sats="2000">2000</button>
|
||||
<button class="session-amount-btn" data-sats="5000">5000</button>
|
||||
<button class="session-amount-btn" data-sats="10000">10k</button>
|
||||
</div>
|
||||
<div class="amount-tag" id="session-topup-display">500 sats</div>
|
||||
<button class="panel-btn primary-green" id="session-topup-create-btn" style="margin-top:14px">CREATE TOPUP INVOICE →</button>
|
||||
<div class="session-amount-row">
|
||||
<input type="number" id="session-topup-input" class="session-amount-input"
|
||||
min="200" max="10000" value="500" step="1" />
|
||||
<span>sats</span>
|
||||
</div>
|
||||
<button class="panel-btn primary-green" id="session-topup-create-btn" style="margin-top:12px">CREATE TOPUP INVOICE →</button>
|
||||
|
||||
<div id="session-topup-pr-row" style="display:none">
|
||||
<div class="panel-label" style="margin-top:14px">TOPUP INVOICE</div>
|
||||
|
||||
@@ -48,31 +48,52 @@ export function initSessionPanel() {
|
||||
_on('session-topup-pay-btn', 'click', _payTopup);
|
||||
_on('session-back-btn', 'click', () => _setStep('active'));
|
||||
_on('topup-quick-btn', 'click', () => { _openPanel(); _setStep('topup'); });
|
||||
_on('session-hud-topup', 'click', (e) => { e.preventDefault(); _openPanel(); _setStep('topup'); });
|
||||
|
||||
// Amount preset buttons — deposit
|
||||
// Amount preset buttons — deposit (quick-fill the number input)
|
||||
_panel.querySelectorAll('[data-session-step="fund"] .session-amount-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
_panel.querySelectorAll('[data-session-step="fund"] .session-amount-btn')
|
||||
.forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
_selectedSats = parseInt(btn.dataset.sats, 10);
|
||||
const el = document.getElementById('session-amount-display');
|
||||
if (el) el.textContent = _selectedSats + ' sats';
|
||||
const inp = document.getElementById('session-amount-input');
|
||||
if (inp) inp.value = _selectedSats;
|
||||
});
|
||||
});
|
||||
|
||||
// Amount preset buttons — topup
|
||||
// Free-text number input — deposit
|
||||
document.getElementById('session-amount-input')?.addEventListener('input', () => {
|
||||
const v = parseInt(document.getElementById('session-amount-input').value, 10);
|
||||
if (Number.isFinite(v)) {
|
||||
_selectedSats = v;
|
||||
_panel.querySelectorAll('[data-session-step="fund"] .session-amount-btn')
|
||||
.forEach(b => b.classList.toggle('active', parseInt(b.dataset.sats, 10) === v));
|
||||
}
|
||||
});
|
||||
|
||||
// Amount preset buttons — topup (quick-fill the topup number input)
|
||||
_panel.querySelectorAll('[data-session-step="topup"] .session-amount-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
_panel.querySelectorAll('[data-session-step="topup"] .session-amount-btn')
|
||||
.forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
_topupSats = parseInt(btn.dataset.sats, 10);
|
||||
const el = document.getElementById('session-topup-display');
|
||||
if (el) el.textContent = _topupSats + ' sats';
|
||||
const inp = document.getElementById('session-topup-input');
|
||||
if (inp) inp.value = _topupSats;
|
||||
});
|
||||
});
|
||||
|
||||
// Free-text number input — topup
|
||||
document.getElementById('session-topup-input')?.addEventListener('input', () => {
|
||||
const v = parseInt(document.getElementById('session-topup-input').value, 10);
|
||||
if (Number.isFinite(v)) {
|
||||
_topupSats = v;
|
||||
_panel.querySelectorAll('[data-session-step="topup"] .session-amount-btn')
|
||||
.forEach(b => b.classList.toggle('active', parseInt(b.dataset.sats, 10) === v));
|
||||
}
|
||||
});
|
||||
|
||||
// Try to restore from localStorage on init
|
||||
_tryRestore();
|
||||
}
|
||||
@@ -168,6 +189,16 @@ function _closePanel() {
|
||||
|
||||
async function _createSession() {
|
||||
_clearError();
|
||||
// Read from the number input (may differ from the last preset clicked)
|
||||
const inp = document.getElementById('session-amount-input');
|
||||
if (inp) {
|
||||
const v = parseInt(inp.value, 10);
|
||||
if (Number.isFinite(v)) _selectedSats = v;
|
||||
}
|
||||
if (_selectedSats < 200 || _selectedSats > 10_000) {
|
||||
_setError('Amount must be 200–10,000 sats.');
|
||||
return;
|
||||
}
|
||||
_setStatus('fund', 'creating session…', '#ffaa00');
|
||||
_btn('session-create-btn', true);
|
||||
|
||||
@@ -239,9 +270,8 @@ function _startDepositPolling() {
|
||||
_sessionState = 'active';
|
||||
_saveToStorage();
|
||||
_applySessionUI();
|
||||
_setStep('active');
|
||||
_updateActiveStep();
|
||||
_setStatus('active', '⚡ Session live!', '#22aa66');
|
||||
_closePanel(); // panel auto-closes; user types in input bar
|
||||
appendSystemMessage(`Session active — ${_balanceSats} sats`);
|
||||
return;
|
||||
}
|
||||
} catch { /* network hiccup */ }
|
||||
@@ -261,6 +291,16 @@ function _startDepositPolling() {
|
||||
async function _createTopup() {
|
||||
if (!_sessionId || !_macaroon) return;
|
||||
_clearError();
|
||||
// Read from the topup number input
|
||||
const inp = document.getElementById('session-topup-input');
|
||||
if (inp) {
|
||||
const v = parseInt(inp.value, 10);
|
||||
if (Number.isFinite(v)) _topupSats = v;
|
||||
}
|
||||
if (_topupSats < 200 || _topupSats > 10_000) {
|
||||
_setError('Topup amount must be 200–10,000 sats.');
|
||||
return;
|
||||
}
|
||||
_setStatus('topup', 'creating topup invoice…', '#ffaa00');
|
||||
_btn('session-topup-create-btn', true);
|
||||
|
||||
@@ -389,32 +429,35 @@ async function _tryRestore() {
|
||||
// ── Session UI helpers ────────────────────────────────────────────────────────
|
||||
|
||||
function _applySessionUI() {
|
||||
const active = _sessionState === 'active';
|
||||
const lowBal = active && _balanceSats < MIN_BALANCE;
|
||||
const anyActive = isSessionActive(); // true for both 'active' and 'paused'
|
||||
const canSend = _sessionState === 'active';
|
||||
// Show low-balance overlay whenever session exists but balance is too low
|
||||
const lowBal = anyActive && _balanceSats < MIN_BALANCE;
|
||||
|
||||
// HUD balance
|
||||
const $bal = document.getElementById('session-hud');
|
||||
if ($bal) {
|
||||
$bal.style.display = isSessionActive() ? '' : 'none';
|
||||
$bal.textContent = `SESSION ${_balanceSats} ⚡`;
|
||||
// HUD balance: "Balance: X sats ⚡ Top Up"
|
||||
const $hud = document.getElementById('session-hud');
|
||||
if ($hud) {
|
||||
$hud.style.display = anyActive ? '' : 'none';
|
||||
const $span = document.getElementById('session-hud-balance');
|
||||
if ($span) $span.textContent = `Balance: ${_balanceSats} sats`;
|
||||
}
|
||||
|
||||
// Open session button label
|
||||
// Top button label
|
||||
const $btn = document.getElementById('open-session-btn');
|
||||
if ($btn) {
|
||||
$btn.textContent = isSessionActive()
|
||||
? `⚡ ${_balanceSats} sats`
|
||||
$btn.textContent = anyActive
|
||||
? `SESSION: ${_balanceSats} ⚡`
|
||||
: '⚡ FUND SESSION';
|
||||
$btn.style.background = isSessionActive() ? '#1a4a30' : '';
|
||||
$btn.style.background = anyActive ? '#1a4a30' : '';
|
||||
}
|
||||
|
||||
// Input bar session mode
|
||||
setInputBarSessionMode(active, 'Ask Timmy (session active)…');
|
||||
// Input bar green border + placeholder when session can send
|
||||
setInputBarSessionMode(canSend, 'Ask Timmy (session active)…');
|
||||
|
||||
// Wire or un-wire the session send handler
|
||||
setSessionSendHandler(active ? sessionSendHandler : null);
|
||||
// Route input bar to session handler only when active (not paused)
|
||||
setSessionSendHandler(canSend ? sessionSendHandler : null);
|
||||
|
||||
// Low balance notice
|
||||
// Low balance notice above input bar
|
||||
const $notice = document.getElementById('low-balance-notice');
|
||||
if ($notice) $notice.style.display = lowBal ? '' : 'none';
|
||||
}
|
||||
@@ -438,8 +481,8 @@ function _clearSession() {
|
||||
localStorage.removeItem(LS_KEY);
|
||||
setInputBarSessionMode(false);
|
||||
setSessionSendHandler(null);
|
||||
const $bal = document.getElementById('session-hud');
|
||||
if ($bal) $bal.style.display = 'none';
|
||||
const $hud = document.getElementById('session-hud');
|
||||
if ($hud) $hud.style.display = 'none';
|
||||
const $btn = document.getElementById('open-session-btn');
|
||||
if ($btn) { $btn.textContent = '⚡ FUND SESSION'; $btn.style.background = ''; }
|
||||
const $notice = document.getElementById('low-balance-notice');
|
||||
|
||||
Reference in New Issue
Block a user