diff --git a/the-matrix/index.html b/the-matrix/index.html index fdfd704..a2b112b 100644 --- a/the-matrix/index.html +++ b/the-matrix/index.html @@ -37,6 +37,12 @@ font-size: 13px; letter-spacing: 3px; margin-bottom: 4px; color: #7799cc; text-shadow: 0 0 10px #4466aa; } + #session-hud { + display: none; + color: #22aa66; + text-shadow: 0 0 6px #11663388; + letter-spacing: 1px; + } #connection-status { position: fixed; top: 16px; right: 16px; @@ -58,12 +64,15 @@ } .log-entry { opacity: 0.7; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } - /* ── Open payment panel button ───────────────────────────────────── */ - #open-panel-btn { + /* ── Top button bar ───────────────────────────────────────────────── */ + #top-buttons { position: fixed; top: 16px; left: 50%; transform: translateX(-50%); + display: flex; gap: 8px; z-index: 20; + } + #open-panel-btn { font-family: 'Courier New', monospace; font-size: 11px; font-weight: bold; color: #000; background: #4466aa; border: none; - padding: 7px 18px; cursor: pointer; z-index: 20; letter-spacing: 2px; + padding: 7px 18px; cursor: pointer; letter-spacing: 2px; box-shadow: 0 0 14px #2244aa66; transition: background 0.15s, box-shadow 0.15s; border-radius: 2px; @@ -73,6 +82,42 @@ background: #5577cc; box-shadow: 0 0 20px #3355aa88; } + #open-session-btn { + font-family: 'Courier New', monospace; font-size: 11px; font-weight: bold; + color: #d0ffe0; background: #0d3322; border: 1px solid #22aa66; + padding: 7px 18px; cursor: pointer; letter-spacing: 1px; + box-shadow: 0 0 14px #0a441a44; + transition: background 0.15s, box-shadow 0.15s, color 0.15s; + border-radius: 2px; + min-height: 36px; + } + #open-session-btn:hover, #open-session-btn:active { + background: #1a4a30; + box-shadow: 0 0 20px #22aa6666; + color: #88ffcc; + } + + /* ── Low balance notice ───────────────────────────────────────────── */ + #low-balance-notice { + display: none; + position: fixed; bottom: 65px; left: 0; right: 0; + text-align: center; + background: rgba(120, 50, 10, 0.92); + color: #ffcc80; + font-size: 11px; letter-spacing: 1px; + padding: 6px 12px; + z-index: 25; + border-top: 1px solid #aa6622; + } + #low-balance-notice button { + background: transparent; border: 1px solid #ffcc80; + color: #ffcc80; font-family: 'Courier New', monospace; + font-size: 11px; padding: 2px 10px; cursor: pointer; + margin-left: 8px; letter-spacing: 1px; + transition: background 0.15s; + pointer-events: all; + } + #low-balance-notice button:hover { background: rgba(255,200,100,0.15); } /* ── Input bar ───────────────────────────────────────────────────── */ #input-bar { @@ -96,11 +141,21 @@ outline: none; min-height: 44px; border-radius: 3px; - transition: border-color 0.2s; + transition: border-color 0.2s, box-shadow 0.4s; -webkit-appearance: none; } #visitor-input::placeholder { color: #333355; } #visitor-input:focus { border-color: #4466aa; } + #visitor-input.session-active { + border-color: #22aa66; + box-shadow: 0 0 10px #22aa6630, inset 0 0 4px #22aa6618; + animation: session-pulse 3s ease-in-out infinite; + } + @keyframes session-pulse { + 0%, 100% { box-shadow: 0 0 10px #22aa6630, inset 0 0 4px #22aa6618; } + 50% { box-shadow: 0 0 22px #22aa6670, inset 0 0 8px #22aa6630; } + } + #visitor-input.session-active::placeholder { color: #226644; } #send-btn { background: rgba(30, 40, 80, 0.9); border: 1px solid #2a2a44; @@ -118,8 +173,9 @@ border-color: #4466aa; color: #88aadd; } + #send-btn:disabled { opacity: 0.35; cursor: not-allowed; } - /* ── Payment panel ────────────────────────────────────────────────── */ + /* ── Payment panel (right side) ───────────────────────────────────── */ #payment-panel { position: fixed; top: 0; right: -420px; width: 400px; height: 100%; @@ -146,7 +202,64 @@ } #payment-close:hover { color: #6688bb; border-color: #4466aa; } + /* ── Session panel (left side) ────────────────────────────────────── */ + #session-panel { + position: fixed; top: 0; left: -420px; + width: 400px; height: 100%; + background: rgba(3, 8, 5, 0.97); + border-right: 1px solid #0e2318; + padding: 24px 20px; + overflow-y: auto; z-index: 100; + font-family: 'Courier New', monospace; + transition: left 0.35s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 8px 0 32px rgba(10, 50, 25, 0.20); + } + #session-panel.open { left: 0; } + #session-panel h2 { + font-size: 13px; letter-spacing: 3px; color: #33bb77; + text-shadow: 0 0 10px #116633; + margin-bottom: 20px; border-bottom: 1px solid #0e2318; padding-bottom: 10px; + } + #session-close { + position: absolute; top: 16px; right: 16px; + background: transparent; border: 1px solid #0e2318; + color: #226644; font-family: 'Courier New', monospace; + font-size: 16px; width: 28px; height: 28px; + cursor: pointer; transition: color 0.2s, border-color 0.2s; + } + #session-close:hover { color: #44dd88; border-color: #22aa66; } + + /* Amount presets */ + .session-amount-presets { + display: flex; gap: 6px; flex-wrap: wrap; margin: 10px 0; + } + .session-amount-btn { + background: transparent; border: 1px solid #0e2318; + color: #226644; font-family: 'Courier New', monospace; + font-size: 11px; letter-spacing: 1px; padding: 5px 11px; + cursor: pointer; transition: all 0.15s; border-radius: 2px; + } + .session-amount-btn:hover { background: #0e2318; border-color: #22aa66; color: #44dd88; } + .session-amount-btn.active { background: #0e2318; border-color: #22aa66; color: #44dd88; } + + /* Active session balance tag */ + .amount-tag.session-green { border-color: #22aa66; color: #44dd88; } + + /* Session status / error lines */ + #session-status-fund, + #session-status-invoice, + #session-status-active, + #session-status-topup { + font-size: 11px; margin-top: 8px; min-height: 16px; color: #22aa66; + } + #session-error { + font-size: 11px; margin-top: 8px; min-height: 16px; color: #994444; + } + + /* ── Shared panel primitives ──────────────────────────────────────── */ .panel-label { font-size: 10px; letter-spacing: 2px; color: #334466; margin-bottom: 6px; margin-top: 16px; } + #session-panel .panel-label { color: #1a4430; } + #job-input { width: 100%; background: #060310; border: 1px solid #1a1a2e; color: #aabbdd; font-family: 'Courier New', monospace; font-size: 12px; @@ -167,7 +280,11 @@ .panel-btn:disabled { opacity: 0.35; cursor: not-allowed; } .panel-btn.primary { border-color: #4466aa; color: #7799cc; } .panel-btn.primary:hover:not(:disabled) { background: #4466aa; color: #fff; } + .panel-btn.primary-green { border-color: #22aa66; color: #44dd88; } + .panel-btn.primary-green:hover:not(:disabled) { background: #22aa66; color: #000; } .panel-btn.danger { border-color: #663333; color: #995555; } + .panel-btn.muted { border-color: #0e2318; color: #226644; } + .panel-btn.muted:hover:not(:disabled) { background: #0e2318; color: #44dd88; } #job-status { font-size: 11px; margin-top: 8px; color: #5577aa; min-height: 16px; } #job-error { font-size: 11px; margin-top: 4px; min-height: 16px; color: #994444; } @@ -177,6 +294,9 @@ padding: 10px; margin-top: 8px; font-size: 10px; color: #334466; word-break: break-all; max-height: 80px; overflow-y: auto; } + #session-panel .invoice-box { + background: #020806; border-color: #0e2318; color: #1a4430; + } .copy-row { display: flex; gap: 8px; margin-top: 6px; align-items: stretch; } .copy-row .invoice-box { flex: 1; margin-top: 0; } .copy-btn { @@ -185,12 +305,18 @@ padding: 0 10px; cursor: pointer; transition: all 0.2s; white-space: nowrap; } .copy-btn:hover { border-color: #4466aa; color: #6688bb; } + #session-panel .copy-btn { border-color: #0e2318; color: #1a4430; } + #session-panel .copy-btn:hover { border-color: #22aa66; color: #44dd88; } + .amount-tag { display: inline-block; background: #0a0820; border: 1px solid #334466; color: #6688bb; font-size: 16px; font-weight: bold; letter-spacing: 2px; padding: 6px 14px; margin-top: 8px; } + #session-panel .amount-tag { + background: #020806; border-color: #22aa66; color: #44dd88; + } #job-result { background: #060310; border: 1px solid #1a1a2e; color: #aabbdd; padding: 12px; font-size: 12px; @@ -227,12 +353,23 @@

THE WORKSHOP

FPS: --
JOBS: 0
+
OFFLINE
- + +
+ + +
+ + +
+ ⚡ Low balance — + +
@@ -240,7 +377,7 @@
- +

⚡ TIMMY — JOB SUBMISSION

@@ -286,6 +423,81 @@
+ +
+ +

⚡ TIMMY — SESSION

+ + +
+
DEPOSIT AMOUNT
+
+ + + + + +
+
500 sats
+ +
+
+ + +
+
DEPOSIT AMOUNT
+
-- sats
+
LIGHTNING INVOICE
+
+
+ +
+ + +
+
+ + +
+
BALANCE
+
-- sats
+

+ TYPE IN THE INPUT BAR TO ASK TIMMY.
EACH REQUEST DEDUCTS FROM YOUR BALANCE. +

+ +
+
+ + +
+
TOPUP AMOUNT
+
+ + + + + +
+
500 sats
+ + + + +
+ +
+ +
+
+
GPU context lost — recovering...
diff --git a/the-matrix/js/main.js b/the-matrix/js/main.js index 8831618..40ff815 100644 --- a/the-matrix/js/main.js +++ b/the-matrix/js/main.js @@ -9,6 +9,7 @@ import { initUI, updateUI } from './ui.js'; import { initInteraction, disposeInteraction, registerSlapTarget } from './interaction.js'; import { initWebSocket, getConnectionState, getJobCount } from './websocket.js'; import { initPaymentPanel } from './payment.js'; +import { initSessionPanel } from './session.js'; let running = false; let canvas = null; @@ -29,6 +30,7 @@ function buildWorld(firstInit, stateSnapshot) { initUI(); initWebSocket(scene); initPaymentPanel(); + initSessionPanel(); } const ac = new AbortController(); diff --git a/the-matrix/js/session.js b/the-matrix/js/session.js new file mode 100644 index 0000000..53314e3 --- /dev/null +++ b/the-matrix/js/session.js @@ -0,0 +1,502 @@ +/** + * session.js — Workshop Session Mode UI (Fund once, ask many) + * + * Flow: + * 1. User clicks "⚡ FUND SESSION" → left panel slides in + * 2. Picks amount (200–5000 sats) → POST /api/sessions → deposit invoice + * 3. Pays invoice (stub: "Simulate Payment") → 2 s polling until state=active + * 4. Macaroon + sessionId stored in localStorage; input bar activates (green border) + * 5. Every Enter/Send in the input bar → POST /api/sessions/:id/request + * 6. Timmy's response shown in speech bubble, balance ticks down in HUD + * 7. Balance < 50 sats → low-balance notice; Top Up button reopens panel topup step + * 8. On page reload: localStorage is read, session is validated, UI restored + */ + +import { setSpeechBubble, setAgentState } from './agents.js'; +import { appendSystemMessage, setSessionSendHandler, setInputBarSessionMode } from './ui.js'; + +const API = '/api'; +const LS_KEY = 'timmy_session_v1'; +const POLL_MS = 2000; +const POLL_TIMEOUT = 60_000; +const MIN_BALANCE = 50; + +// ── Module state ────────────────────────────────────────────────────────────── +let _panel = null; +let _sessionId = null; +let _macaroon = null; +let _balanceSats = 0; +let _sessionState = null; // null | 'awaiting_payment' | 'active' | 'paused' | 'expired' +let _pollTimer = null; +let _inFlight = false; +let _selectedSats = 500; // deposit amount selection +let _topupSats = 500; // topup amount selection + +// ── Public API ──────────────────────────────────────────────────────────────── + +export function initSessionPanel() { + _panel = document.getElementById('session-panel'); + if (!_panel) return; + + // Buttons + _on('open-session-btn', 'click', _openPanel); + _on('session-close', 'click', _closePanel); + _on('session-create-btn', 'click', _createSession); + _on('session-pay-btn', 'click', _payDeposit); + _on('session-topup-btn', 'click', () => _setStep('topup')); + _on('session-topup-create-btn','click', _createTopup); + _on('session-topup-pay-btn', 'click', _payTopup); + _on('session-back-btn', 'click', () => _setStep('active')); + _on('topup-quick-btn', 'click', () => { _openPanel(); _setStep('topup'); }); + + // Amount preset buttons — deposit + _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'; + }); + }); + + // Amount preset buttons — topup + _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'; + }); + }); + + // Try to restore from localStorage on init + _tryRestore(); +} + +export function isSessionActive() { + return _sessionState === 'active' || _sessionState === 'paused'; +} + +// Called by ui.js when user submits the input bar while session is active +export async function sessionSendHandler(text) { + if (!_sessionId || !_macaroon || _inFlight) return; + + _inFlight = true; + _setSendBusy(true); + appendSystemMessage(`you: ${text}`); + + try { + const res = await fetch(`${API}/sessions/${_sessionId}/request`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${_macaroon}`, + }, + body: JSON.stringify({ request: text }), + }); + + const data = await res.json(); + + if (res.status === 402) { + _balanceSats = data.balance ?? 0; + _sessionState = 'paused'; + _saveToStorage(); + _applySessionUI(); + setSpeechBubble('My energy is depleted... top me up to continue!'); + appendSystemMessage('⚡ Low balance — tap Top Up'); + return; + } + + if (res.status === 401) { + // Macaroon rejected — stale session, clear and reset + _clearSession(); + appendSystemMessage('Session expired — fund a new one.'); + return; + } + + if (!res.ok) { + appendSystemMessage('Session error: ' + (data.error || res.status)); + return; + } + + _balanceSats = data.balanceRemaining ?? 0; + _sessionState = _balanceSats < MIN_BALANCE ? 'paused' : 'active'; + _saveToStorage(); + _applySessionUI(); + + const reply = data.result || data.reason || '…'; + setSpeechBubble(reply); + appendSystemMessage('Timmy: ' + reply.slice(0, 80)); + + // Update active-step balance if panel is open + _updateActiveStep(); + + } catch (err) { + appendSystemMessage('Session error: ' + err.message); + } finally { + _inFlight = false; + _setSendBusy(false); + } +} + +// ── Panel open/close ────────────────────────────────────────────────────────── + +function _openPanel() { + if (!_panel) return; + _panel.classList.add('open'); + _clearError(); + if (_sessionState === 'active') { + _setStep('active'); + _updateActiveStep(); + } else if (_sessionState === 'paused') { + _setStep('topup'); + } else { + _setStep('fund'); + } +} + +function _closePanel() { + _panel?.classList.remove('open'); + _stopPolling(); +} + +// ── Create session ──────────────────────────────────────────────────────────── + +async function _createSession() { + _clearError(); + _setStatus('fund', 'creating session…', '#ffaa00'); + _btn('session-create-btn', true); + + try { + const res = await fetch(`${API}/sessions`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ amount_sats: _selectedSats }), + }); + const data = await res.json(); + + if (!res.ok) { _setError(data.error || 'Failed to create session.'); return; } + + _sessionId = data.sessionId; + + // Show deposit invoice + const inv = data.invoice; + _el('session-invoice-amount').textContent = inv.amountSats + ' sats'; + _el('session-invoice-pr').textContent = inv.paymentRequest || ''; + const hashEl = _el('session-invoice-hash'); + if (hashEl) hashEl.dataset.hash = inv.paymentHash || ''; + + _setStep('invoice'); + _setStatus('invoice', '⚡ Awaiting deposit…', '#ffaa00'); + + } catch (err) { + _setError('Network error: ' + err.message); + } finally { + _btn('session-create-btn', false); + } +} + +// ── Pay deposit (stub) ──────────────────────────────────────────────────────── + +async function _payDeposit() { + const hash = _el('session-invoice-hash')?.dataset.hash; + if (!hash) { _setError('No payment hash — use real Lightning?'); return; } + + _btn('session-pay-btn', true); + _setStatus('invoice', 'Simulating payment…', '#ffaa00'); + + try { + const res = await fetch(`${API}/dev/stub/pay/${hash}`, { method: 'POST' }); + const ok = (await res.json()).ok; + if (!ok) { _setError('Payment simulation failed.'); return; } + _startDepositPolling(); + } catch (err) { + _setError('Payment error: ' + err.message); + } finally { + _btn('session-pay-btn', false); + } +} + +// ── Deposit polling ─────────────────────────────────────────────────────────── + +function _startDepositPolling() { + _stopPolling(); + const deadline = Date.now() + POLL_TIMEOUT; + + async function poll() { + if (!_sessionId) return; + try { + const res = await fetch(`${API}/sessions/${_sessionId}`); + const data = await res.json(); + + if (data.state === 'active') { + _macaroon = data.macaroon; + _balanceSats = data.balanceSats; + _sessionState = 'active'; + _saveToStorage(); + _applySessionUI(); + _setStep('active'); + _updateActiveStep(); + _setStatus('active', '⚡ Session live!', '#22aa66'); + return; + } + } catch { /* network hiccup */ } + + if (Date.now() > deadline) { + _setError('Timed out waiting for payment.'); + return; + } + _pollTimer = setTimeout(poll, POLL_MS); + } + + _pollTimer = setTimeout(poll, POLL_MS); +} + +// ── Topup ───────────────────────────────────────────────────────────────────── + +async function _createTopup() { + if (!_sessionId || !_macaroon) return; + _clearError(); + _setStatus('topup', 'creating topup invoice…', '#ffaa00'); + _btn('session-topup-create-btn', true); + + try { + const res = await fetch(`${API}/sessions/${_sessionId}/topup`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${_macaroon}`, + }, + body: JSON.stringify({ amount_sats: _topupSats }), + }); + const data = await res.json(); + + if (!res.ok) { + // Pending topup already exists — surface it + const pending = data.pendingTopup; + if (pending) { + _el('session-topup-pr').textContent = pending.paymentRequest || ''; + const th = _el('session-topup-hash'); + if (th) th.dataset.hash = pending.paymentHash || ''; + _el('session-topup-pr-row').style.display = ''; + _setStatus('topup', '⚡ Previous invoice still pending', '#ffaa00'); + return; + } + _setError(data.error || 'Failed to create topup.'); + return; + } + + const inv = data.topup; + _el('session-topup-pr').textContent = inv.paymentRequest || ''; + const th = _el('session-topup-hash'); + if (th) th.dataset.hash = inv.paymentHash || ''; + _el('session-topup-pr-row').style.display = ''; + _setStatus('topup', '⚡ Awaiting topup payment…', '#ffaa00'); + + } catch (err) { + _setError('Topup error: ' + err.message); + } finally { + _btn('session-topup-create-btn', false); + } +} + +async function _payTopup() { + const hash = _el('session-topup-hash')?.dataset.hash; + if (!hash) { _setError('No topup hash.'); return; } + + _btn('session-topup-pay-btn', true); + _setStatus('topup', 'Simulating topup payment…', '#ffaa00'); + + try { + const res = await fetch(`${API}/dev/stub/pay/${hash}`, { method: 'POST' }); + const ok = (await res.json()).ok; + if (!ok) { _setError('Topup simulation failed.'); return; } + _startTopupPolling(); + } catch (err) { + _setError('Topup payment error: ' + err.message); + } finally { + _btn('session-topup-pay-btn', false); + } +} + +function _startTopupPolling() { + _stopPolling(); + const deadline = Date.now() + POLL_TIMEOUT; + const prevBalance = _balanceSats; + + async function poll() { + if (!_sessionId) return; + try { + const res = await fetch(`${API}/sessions/${_sessionId}`); + const data = await res.json(); + + if (data.balanceSats > prevBalance || data.state === 'active') { + _balanceSats = data.balanceSats; + _macaroon = data.macaroon || _macaroon; + _sessionState = data.state === 'active' ? 'active' : _sessionState; + _saveToStorage(); + _applySessionUI(); + _setStep('active'); + _updateActiveStep(); + _setStatus('active', `⚡ Topped up! ${_balanceSats} sats`, '#22aa66'); + return; + } + } catch { /* ignore */ } + + if (Date.now() > deadline) { _setError('Topup timed out.'); return; } + _pollTimer = setTimeout(poll, POLL_MS); + } + + _pollTimer = setTimeout(poll, POLL_MS); +} + +// ── Restore from localStorage ───────────────────────────────────────────────── + +async function _tryRestore() { + try { + const raw = localStorage.getItem(LS_KEY); + if (!raw) return; + const { sessionId, macaroon, balanceSats } = JSON.parse(raw); + if (!sessionId || !macaroon) return; + + // Validate the session is still live + const res = await fetch(`${API}/sessions/${sessionId}`); + if (!res.ok) { localStorage.removeItem(LS_KEY); return; } + const data = await res.json(); + + if (data.state !== 'active' && data.state !== 'paused') { + localStorage.removeItem(LS_KEY); + return; + } + + _sessionId = sessionId; + _macaroon = data.macaroon || macaroon; + _balanceSats = data.balanceSats ?? balanceSats; + _sessionState = data.state; + _saveToStorage(); + _applySessionUI(); + + appendSystemMessage(`Session restored (${_balanceSats} sats)`); + } catch { + // Ignore restore errors silently + } +} + +// ── Session UI helpers ──────────────────────────────────────────────────────── + +function _applySessionUI() { + const active = _sessionState === 'active'; + const lowBal = active && _balanceSats < MIN_BALANCE; + + // HUD balance + const $bal = document.getElementById('session-hud'); + if ($bal) { + $bal.style.display = isSessionActive() ? '' : 'none'; + $bal.textContent = `SESSION ${_balanceSats} ⚡`; + } + + // Open session button label + const $btn = document.getElementById('open-session-btn'); + if ($btn) { + $btn.textContent = isSessionActive() + ? `⚡ ${_balanceSats} sats` + : '⚡ FUND SESSION'; + $btn.style.background = isSessionActive() ? '#1a4a30' : ''; + } + + // Input bar session mode + setInputBarSessionMode(active, 'Ask Timmy (session active)…'); + + // Wire or un-wire the session send handler + setSessionSendHandler(active ? sessionSendHandler : null); + + // Low balance notice + const $notice = document.getElementById('low-balance-notice'); + if ($notice) $notice.style.display = lowBal ? '' : 'none'; +} + +function _updateActiveStep() { + const el = document.getElementById('session-active-amount'); + if (el) { + el.textContent = _balanceSats + ' sats'; + el.style.color = _balanceSats < MIN_BALANCE ? '#ff8844' : '#44dd88'; + } + // Reset topup pr row visibility + const prRow = document.getElementById('session-topup-pr-row'); + if (prRow) prRow.style.display = 'none'; +} + +function _clearSession() { + _sessionId = null; + _macaroon = null; + _balanceSats = 0; + _sessionState = null; + localStorage.removeItem(LS_KEY); + setInputBarSessionMode(false); + setSessionSendHandler(null); + const $bal = document.getElementById('session-hud'); + if ($bal) $bal.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'); + if ($notice) $notice.style.display = 'none'; +} + +function _saveToStorage() { + try { + localStorage.setItem(LS_KEY, JSON.stringify({ + sessionId: _sessionId, + macaroon: _macaroon, + balanceSats: _balanceSats, + })); + } catch { /* storage unavailable */ } +} + +// ── DOM helpers ─────────────────────────────────────────────────────────────── + +function _el(id) { return document.getElementById(id); } + +function _on(id, ev, fn) { + _el(id)?.addEventListener(ev, fn); +} + +function _btn(id, disabled) { + const el = _el(id); + if (el) el.disabled = disabled; +} + +function _setSendBusy(busy) { + const btn = document.getElementById('send-btn'); + if (btn) btn.disabled = busy; +} + +function _setStep(step) { + if (!_panel) return; + _panel.querySelectorAll('[data-session-step]').forEach(el => { + el.style.display = el.dataset.sessionStep === step ? '' : 'none'; + }); +} + +function _setStatus(step, msg, color = '#22aa66') { + const el = document.getElementById(`session-status-${step}`); + if (el) { el.textContent = msg; el.style.color = color; } +} + +function _setError(msg) { + const el = _el('session-error'); + if (el) el.textContent = msg; +} + +function _clearError() { + const el = _el('session-error'); + if (el) el.textContent = ''; +} + +function _stopPolling() { + clearTimeout(_pollTimer); + _pollTimer = null; +} diff --git a/the-matrix/js/ui.js b/the-matrix/js/ui.js index a495dee..e918a6a 100644 --- a/the-matrix/js/ui.js +++ b/the-matrix/js/ui.js @@ -9,6 +9,25 @@ const MAX_LOG = 6; const logEntries = []; let uiInitialized = false; +// ── Session-mode send override ──────────────────────────────────────────────── +let _sessionSendHandler = null; + +export function setSessionSendHandler(fn) { + _sessionSendHandler = fn; +} + +export function setInputBarSessionMode(active, placeholder) { + const $input = document.getElementById('visitor-input'); + if (!$input) return; + if (active) { + $input.classList.add('session-active'); + $input.placeholder = placeholder || 'Ask Timmy (session active)…'; + } else { + $input.classList.remove('session-active'); + $input.placeholder = 'Say something to Timmy…'; + } +} + export function initUI() { if (uiInitialized) return; uiInitialized = true; @@ -24,8 +43,12 @@ function initInputBar() { const text = $input.value.trim(); if (!text) return; $input.value = ''; - sendVisitorMessage(text); - appendSystemMessage(`you: ${text}`); + if (_sessionSendHandler) { + _sessionSendHandler(text); + } else { + sendVisitorMessage(text); + appendSystemMessage(`you: ${text}`); + } } $sendBtn.addEventListener('click', send);