Files
timmy-tower/the-matrix/index.html
Alexander Whitestone 31748cb388 WIP: Gemini Code progress on #10
Automated salvage commit — agent session ended (exit 124).
Work in progress, may need continuation.
2026-03-23 19:31:25 -04:00

965 lines
40 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>The Workshop — Timmy</title>
<link rel="manifest" href="/tower/manifest.json" />
<meta name="theme-color" content="#0a0610" />
<!-- iOS PWA -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="The Workshop" />
<link rel="apple-touch-icon" href="/tower/icons/icon-192.png" />
<style>
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #080610;
overflow: hidden;
font-family: 'Courier New', monospace;
touch-action: none;
user-select: none;
-webkit-user-select: none;
}
canvas { display: block; }
/* ── HUD ─────────────────────────────────────────────────────────── */
#hud {
position: fixed; top: 16px; left: 16px;
color: #5588bb; font-size: 11px; line-height: 1.7;
text-shadow: 0 0 6px #2244aa;
pointer-events: none; z-index: 10;
}
#hud h1 {
font-size: 13px; letter-spacing: 3px; margin-bottom: 4px;
color: #7799cc; text-shadow: 0 0 10px #4466aa;
}
/* Nostr Identity UI */
.nostr-btn {
background: rgba(40, 30, 70, 0.9);
border: 1px solid #443377;
color: #aaddff; font-family: 'Courier New', monospace;
font-size: 11px; padding: 4px 10px; cursor: pointer;
border-radius: 3px; transition: background 0.15s, border-color 0.15s;
}
.nostr-btn:hover { background: rgba(60, 45, 100, 0.9); border-color: #665599; }
.nostr-btn-sm {
font-size: 9px; padding: 2px 6px; margin-left: 6px; opacity: 0.7;
}
.nostr-btn-sm:hover { opacity: 1; }
.nostr-pubkey {
font-size: 11px; color: #aaddff; margin-right: 6px;
letter-spacing: 0.5px;
}
#session-hud {
display: none;
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;
font-size: 11px; color: #333355;
pointer-events: none; z-index: 10;
text-shadow: none;
}
#connection-status.connected {
color: #5588bb;
text-shadow: 0 0 6px #3366aa;
}
/* ── Event log ────────────────────────────────────────────────────── */
#event-log {
position: fixed; bottom: 80px; left: 16px;
width: 280px; max-height: 100px; overflow-y: auto;
color: #445566; font-size: 10px; line-height: 1.6;
pointer-events: none; z-index: 10;
}
.log-entry { opacity: 0.7; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* ── Debate UI (#21) ──────────────────────────────────────────── */
.debate-entry { opacity: 0.9; font-style: italic; white-space: normal; line-height: 1.4; margin-bottom: 2px; }
.debate-a { color: #7799cc; border-left: 2px solid #4466aa; padding-left: 4px; }
.debate-b { color: #cc7799; border-left: 2px solid #aa4466; padding-left: 4px; }
.debate-verdict { font-weight: bold; font-style: normal; opacity: 1; }
.debate-accepted { color: #44dd88; border-left: 2px solid #22aa66; padding-left: 4px; }
.debate-rejected { color: #dd6644; border-left: 2px solid #aa4422; padding-left: 4px; }
/* ── 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; letter-spacing: 2px;
box-shadow: 0 0 14px #2244aa66;
transition: background 0.15s, box-shadow 0.15s;
border-radius: 2px;
min-height: 36px;
}
#open-panel-btn:hover, #open-panel-btn:active {
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 {
position: fixed; bottom: 0; left: 0; right: 0;
display: flex; align-items: center; gap: 8px;
padding: 10px 16px;
background: rgba(8, 6, 16, 0.88);
border-top: 1px solid #1a1a2e;
z-index: 20;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
#visitor-input {
flex: 1;
background: rgba(20, 16, 36, 0.9);
border: 1px solid #2a2a44;
color: #aabbdd;
font-family: 'Courier New', monospace;
font-size: 14px;
padding: 10px 12px;
outline: none;
min-height: 44px;
border-radius: 3px;
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;
color: #5577aa;
font-family: 'Courier New', monospace;
font-size: 16px;
width: 44px; height: 44px;
cursor: pointer;
border-radius: 3px;
transition: background 0.15s, border-color 0.15s, color 0.15s;
display: flex; align-items: center; justify-content: center;
}
#send-btn:hover, #send-btn:active {
background: rgba(50, 70, 140, 0.9);
border-color: #4466aa;
color: #88aadd;
}
#send-btn:disabled { opacity: 0.35; cursor: not-allowed; }
/* ── Payment panel (right side) ───────────────────────────────────── */
#payment-panel {
position: fixed; top: 0; right: -420px;
width: 400px; height: 100%;
background: rgba(5, 3, 12, 0.97);
border-left: 1px solid #1a1a2e;
padding: 24px 20px;
overflow-y: auto; z-index: 100;
font-family: 'Courier New', monospace;
transition: right 0.35s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: -8px 0 32px rgba(40, 60, 120, 0.15);
}
#payment-panel.open { right: 0; }
#payment-panel h2 {
font-size: 13px; letter-spacing: 3px; color: #6688bb;
text-shadow: 0 0 10px #2244aa;
margin-bottom: 20px; border-bottom: 1px solid #1a1a2e; padding-bottom: 10px;
}
#payment-close {
position: absolute; top: 16px; right: 16px;
background: transparent; border: 1px solid #1a1a2e;
color: #333355; font-family: 'Courier New', monospace;
font-size: 16px; width: 28px; height: 28px;
cursor: pointer; transition: color 0.2s, border-color 0.2s;
}
#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;
}
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; }
/* ── Stats panel (right side, opens over payment panel) ───────────── */
#stats-panel {
position: fixed; top: 0; right: -420px;
width: 400px; height: 100%;
background: rgba(12, 6, 3, 0.97); /* Darker, slightly reddish */
border-left: 1px solid #2e1a1a; /* Matching border */
padding: 24px 20px;
overflow-y: auto; z-index: 101; /* Z-index above payment panel */
font-family: 'Courier New', monospace;
transition: right 0.35s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: -8px 0 32px rgba(120, 60, 40, 0.15); /* Orange-ish shadow */
}
#stats-panel.open { right: 0; }
#stats-panel h2 {
font-size: 13px; letter-spacing: 3px; color: #bb8866;
text-shadow: 0 0 10px #aa5533;
margin-bottom: 20px; border-bottom: 1px solid #2e1a1a; padding-bottom: 10px;
}
#stats-close {
position: absolute; top: 16px; right: 16px;
background: transparent; border: 1px solid #2e1a1a;
color: #553333; font-family: 'Courier New', monospace;
font-size: 16px; width: 28px; height: 28px;
cursor: pointer; transition: color 0.2s, border-color 0.2s;
}
#stats-close:hover { color: #bb8866; border-color: #aa6644; }
.panel-stats-grid {
display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 12px; margin-bottom: 20px;
}
.stat-card {
background: rgba(20, 10, 5, 0.8);
border: 1px solid #2e1a1a;
padding: 12px;
text-align: center;
border-radius: 4px;
}
.stat-card.wide { grid-column: 1 / -1; }
.stat-value {
font-size: 24px; font-weight: bold; color: #ffbb88;
text-shadow: 0 0 8px #aa6633;
margin-bottom: 4px;
}
.stat-label {
font-size: 10px; letter-spacing: 1px; color: #aa7755;
}
.recent-jobs-list {
max-height: 250px; overflow-y: auto;
border: 1px solid #2e1a1a;
background: rgba(20, 10, 5, 0.6);
padding: 10px;
border-radius: 4px;
}
.recent-job-item {
border-bottom: 1px solid #2e1a1a;
padding: 8px 0;
}
.recent-job-item:last-child { border-bottom: none; }
.job-req {
font-size: 11px; color: #ffddbb;
line-height: 1.4; margin-bottom: 4px;
}
.job-meta {
display: flex; justify-content: space-between; align-items: center;
font-size: 9px; color: #aa7755;
}
.job-rating { color: #ffd700; }
.job-sats { color: #f7931a; }
.job-time { color: #aa7755; }
/* 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; }
/* QR placeholder */
.qr-placeholder {
display: flex; align-items: center; justify-content: center;
margin-top: 8px;
background: #020806; border: 1px solid #0e2318;
color: #1a4430; font-size: 11px; letter-spacing: 3px;
height: 80px;
}
/* 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; }
/* 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;
padding: 10px; resize: vertical; min-height: 90px;
outline: none; transition: border-color 0.2s;
}
#job-input:focus { border-color: #4466aa; }
#job-input::placeholder { color: #1a1a2e; }
.panel-btn {
width: 100%; margin-top: 12px;
background: transparent; border: 1px solid #334466;
color: #5577aa; font-family: 'Courier New', monospace;
font-size: 12px; letter-spacing: 2px; padding: 10px;
cursor: pointer; transition: all 0.2s;
}
.panel-btn:hover:not(:disabled) { background: #334466; color: #aabbdd; }
.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; }
.session-link-btn {
background: none; border: none; color: #557755; font-size: 10px;
font-family: inherit; cursor: pointer; margin-top: 10px; padding: 4px 0;
letter-spacing: 1px; display: block;
}
.session-link-btn:hover:not(:disabled) { color: #44dd88; text-decoration: underline; }
.session-link-btn:disabled { opacity: 0.35; cursor: not-allowed; }
#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; }
.invoice-box {
background: #060310; border: 1px solid #1a1a2e;
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 {
background: transparent; border: 1px solid #1a1a2e; color: #334466;
font-family: 'Courier New', monospace; font-size: 10px;
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;
line-height: 1.6; margin-top: 8px;
white-space: pre-wrap; max-height: 260px; overflow-y: auto;
}
.panel-link {
display: block; text-align: center; margin-top: 20px;
font-size: 10px; letter-spacing: 1px; color: #1a1a2e;
text-decoration: none; transition: color 0.2s;
}
.panel-link:hover { color: #5577aa; }
/* ── Relay Admin button ───────────────────────────────────────────── */
#relay-admin-btn {
display: none;
font-family: 'Courier New', monospace; font-size: 11px; font-weight: bold;
color: #f7931a; background: rgba(40, 25, 5, 0.85); border: 1px solid #f7931a55;
padding: 7px 18px; cursor: pointer; letter-spacing: 2px;
box-shadow: 0 0 14px #f7931a22;
transition: background 0.15s, box-shadow 0.15s, color 0.15s;
border-radius: 2px;
min-height: 36px;
text-decoration: none;
}
#relay-admin-btn:hover, #relay-admin-btn:active {
background: rgba(60, 35, 8, 0.95);
box-shadow: 0 0 20px #f7931a44;
color: #ffb347;
}
/* ── AR pulse animation ──────────────────────────────────────────── */
@keyframes ar-pulse {
0%, 100% { opacity: 0.6; transform: scale(1); }
50% { opacity: 1; transform: scale(1.5); }
}
/* ── Crosshair ───────────────────────────────────────────────────── */
#crosshair {
position: fixed; top: 50%; left: 50%;
transform: translate(-50%, -50%);
pointer-events: none; z-index: 12;
opacity: 0.5;
}
#crosshair::before, #crosshair::after {
content: ''; position: absolute;
background: rgba(180, 160, 220, 0.7);
border-radius: 1px;
}
#crosshair::before { width: 16px; height: 1px; top: 0; left: -8px; }
#crosshair::after { width: 1px; height: 16px; top: -8px; left: 0; }
/* ── Lock hint (desktop) ─────────────────────────────────────────── */
#lock-hint {
position: fixed; inset: 0;
display: none;
align-items: center; justify-content: center;
z-index: 11; pointer-events: none;
}
#lock-hint .lock-badge {
background: rgba(8, 6, 20, 0.72);
border: 1px solid rgba(80, 100, 160, 0.5);
border-radius: 8px;
color: #5577aa;
font-family: 'Courier New', monospace;
font-size: 11px; letter-spacing: 2px;
padding: 10px 24px;
text-align: center;
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
animation: lock-fade 2s ease-in-out infinite alternate;
}
@keyframes lock-fade {
0% { opacity: 0.45; }
100% { opacity: 0.9; }
}
/* ── Virtual joystick (mobile) ───────────────────────────────────── */
#joy-pad {
position: fixed;
display: none; /* shown by navigation.js on mobile */
align-items: center; justify-content: center;
width: 110px; height: 110px;
border-radius: 50%;
background: rgba(40, 30, 70, 0.38);
border: 1.5px solid rgba(100, 80, 180, 0.45);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
z-index: 18;
pointer-events: none; /* touches handled on canvas */
opacity: 0.35;
transition: opacity 0.25s;
bottom: 80px; left: 20px;
}
#joy-nub {
width: 38px; height: 38px; border-radius: 50%;
background: rgba(140, 110, 220, 0.65);
border: 1.5px solid rgba(180, 150, 255, 0.6);
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
box-shadow: 0 0 14px rgba(140, 110, 220, 0.5);
transition: transform 0.05s linear;
pointer-events: none;
}
/* ── AR label pulse ──────────────────────────────────────────────── */
.ar-label { transition: opacity 0.25s; }
/* ── WebGL recovery overlay ──────────────────────────────────────── */
#webgl-recovery-overlay {
display: none; position: fixed; inset: 0; z-index: 200;
background: rgba(5, 3, 12, 0.92);
justify-content: center; align-items: center;
pointer-events: none;
}
#webgl-recovery-overlay .recovery-text {
color: #22aa66; font-family: 'Courier New', monospace;
font-size: 15px; letter-spacing: 3px;
text-shadow: 0 0 10px #11663388;
animation: ctx-blink 1.2s step-end infinite;
}
@keyframes ctx-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.2; }
}
/* ── Timmy identity card ──────────────────────────────────────────── */
#timmy-id-card {
position: fixed; bottom: 80px; right: 16px;
font-size: 10px; color: #334466;
pointer-events: all; z-index: 10;
text-align: right; line-height: 1.8;
}
#timmy-id-card .id-label {
letter-spacing: 2px; color: #223355;
text-transform: uppercase; font-size: 9px;
}
#timmy-id-card .id-npub {
color: #4466aa; cursor: pointer;
text-decoration: underline dotted;
font-size: 10px; letter-spacing: 0.5px;
}
#timmy-id-card .id-npub:hover { color: #88aadd; }
#timmy-id-card .id-zaps { color: #556688; font-size: 9px; }
/* ── Activity heatmap (#9) ────────────────────────────────────────── */
#activity-heatmap {
position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%);
z-index: 10; pointer-events: all;
}
#heatmap-bar {
display: flex; gap: 2px; align-items: flex-end;
}
.hm-seg {
width: 10px; height: 18px; border-radius: 1px;
background: #111122;
cursor: pointer;
transition: transform 0.1s;
flex-shrink: 0;
}
.hm-seg:hover { transform: scaleY(1.3); }
@keyframes hm-pulse {
0%, 100% { opacity: 1; box-shadow: 0 0 4px currentColor; }
50% { opacity: 0.5; box-shadow: none; }
}
.hm-seg-current { animation: hm-pulse 2s ease-in-out infinite; }
#heatmap-icon-btn {
display: none;
background: rgba(20, 16, 36, 0.88);
border: 1px solid #2a2a44;
color: #5588bb;
font-family: 'Courier New', monospace;
font-size: 16px; padding: 6px 10px;
cursor: pointer; border-radius: 3px;
}
#heatmap-tooltip {
position: fixed; display: none;
background: rgba(5,3,12,0.92); border: 1px solid #2a2a44;
color: #aabbdd; font-family: 'Courier New', monospace;
font-size: 10px; padding: 3px 8px; border-radius: 2px;
pointer-events: none; z-index: 50;
white-space: nowrap;
}
/* Mobile overlay */
#heatmap-overlay {
display: none; position: fixed; inset: 0;
background: rgba(5,3,12,0.97); z-index: 100;
flex-direction: column; align-items: center; justify-content: center;
gap: 16px;
}
#heatmap-overlay.open { display: flex; }
#heatmap-overlay-title {
color: #7799cc; font-family: 'Courier New', monospace;
font-size: 12px; letter-spacing: 3px;
}
#heatmap-overlay-bar {
display: flex; gap: 4px; align-items: flex-end; flex-wrap: wrap;
justify-content: center; max-width: 90vw;
}
#heatmap-overlay-bar .hm-seg { width: 14px; height: 28px; }
#heatmap-overlay-close {
background: transparent; border: 1px solid #2a2a44;
color: #5588bb; font-family: 'Courier New', monospace;
font-size: 11px; padding: 6px 16px; cursor: pointer;
letter-spacing: 1px; border-radius: 2px;
}
@media (max-width: 600px) {
#activity-heatmap #heatmap-bar { display: none; }
#heatmap-icon-btn { display: block; }
}
</style>
</head>
<body>
<div id="hud">
<h1>THE WORKSHOP</h1>
<div id="fps">FPS: --</div>
<div id="active-jobs">JOBS: 0</div>
<div id="session-hud">
<span id="session-hud-balance">Balance: -- sats</span>
<a href="#" id="session-hud-topup">⚡ Top Up</a>
</div>
<!-- New: Nostr identity status -->
<div id="nostr-identity-status" style="margin-top: 10px; pointer-events: all;"></div>
</div>
<div id="connection-status">OFFLINE</div>
<div id="event-log"></div>
<!-- ── Activity heatmap (#9) ──────────────────────────────────────── -->
<div id="activity-heatmap">
<div id="heatmap-bar"></div>
<button id="heatmap-icon-btn" title="Show activity heatmap"></button>
</div>
<div id="heatmap-tooltip"></div>
<div id="heatmap-overlay">
<div id="heatmap-overlay-title">24H ACTIVITY</div>
<div id="heatmap-overlay-bar"></div>
<button id="heatmap-overlay-close">CLOSE</button>
</div>
<!-- ── Timmy identity card ────────────────────────────────────────── -->
<div id="timmy-id-card">
<div class="id-label">TIMMY IDENTITY</div>
<div class="id-npub" id="timmy-npub" title="Click to copy Timmy's Nostr npub"></div>
<div class="id-zaps" id="timmy-zap-count">⚡ 0 zaps sent</div>
</div>
<!-- ── Top action buttons ─────────────────────────────────────────── -->
<div id="top-buttons">
<button id="open-panel-btn">⚡ SUBMIT JOB</button>
<button id="open-stats-btn">📊 TIMMY STATS</button>
<button id="open-session-btn">⚡ FUND SESSION</button>
<a id="relay-admin-btn" href="/admin/relay">⚙ RELAY ADMIN</a>
</div>
<!-- ── Low balance notice (above input bar) ───────────────────────── -->
<div id="low-balance-notice">
⚡ Low balance —
<button id="topup-quick-btn">Top Up</button>
</div>
<!-- ── Input bar ──────────────────────────────────────────────────── -->
<div id="input-bar">
<input type="text" id="visitor-input" placeholder="Say something to Timmy…" autocomplete="off" autocorrect="off" spellcheck="false" />
<button id="send-btn" aria-label="Send"></button>
</div>
<!-- ── Payment panel (right side) ────────────────────────────────── -->
<div id="payment-panel">
<button id="payment-close"></button>
<h2>⚡ TIMMY — JOB SUBMISSION</h2>
<div data-step="input">
<div class="panel-label">YOUR REQUEST</div>
<textarea id="job-input" maxlength="500" placeholder="Ask Timmy anything… (max 500 chars)"></textarea>
<button class="panel-btn primary" id="job-submit-btn">CREATE JOB →</button>
<a class="panel-link" href="/api/ui" target="_blank">Open full UI ↗</a>
</div>
<div data-step="eval-invoice" style="display:none">
<div class="panel-label">EVAL FEE</div>
<div class="amount-tag" id="eval-amount">10 sats</div>
<div class="panel-label" style="margin-top:12px">LIGHTNING INVOICE</div>
<div class="copy-row">
<div class="invoice-box" id="eval-payment-request"></div>
<button class="copy-btn" onclick="_timmyCopy('eval-payment-request')">COPY</button>
</div>
<span id="eval-hash" data-hash=""></span>
<button class="panel-btn primary" id="pay-eval-btn">⚡ SIMULATE PAYMENT</button>
</div>
<div data-step="work-invoice" style="display:none">
<div class="panel-label">WORK FEE</div>
<div class="amount-tag" id="work-amount">-- sats</div>
<div class="panel-label" style="margin-top:12px">LIGHTNING INVOICE</div>
<div class="copy-row">
<div class="invoice-box" id="work-payment-request"></div>
<button class="copy-btn" onclick="_timmyCopy('work-payment-request')">COPY</button>
</div>
<span id="work-hash" data-hash=""></span>
<button class="panel-btn primary" id="pay-work-btn">⚡ SIMULATE PAYMENT</button>
</div>
<div data-step="result" style="display:none">
<div class="panel-label" id="result-label">AI RESULT</div>
<pre id="job-result"></pre>
<button class="panel-btn" id="new-job-btn" style="margin-top:16px">← NEW JOB</button>
</div>
<div id="job-status"></div>
<div id="job-error"></div>
</div>
<!-- ── Stats panel (right side, opens over payment panel) ──────────────── -->
<div id="stats-panel">
<button id="stats-close"></button>
<h2>📊 TIMMY — PERFORMANCE</h2>
<div class="panel-stats-grid">
<div class="stat-card">
<div class="stat-value" id="total-jobs-completed">--</div>
<div class="stat-label">JOBS COMPLETED</div>
</div>
<div class="stat-card">
<div class="stat-value" id="average-self-eval-rating">--</div>
<div class="stat-label">AVG RATING (1-5)</div>
</div>
<div class="stat-card wide">
<div class="stat-value" id="top-request-categories">--</div>
<div class="stat-label">TOP 3 CATEGORIES</div>
</div>
<div class="stat-card">
<div class="stat-value" id="total-sats-earned">--</div>
<div class="stat-label">TOTAL SAT EARNED</div>
</div>
<div class="stat-card">
<div class="stat-value" id="sats-earned-24h">--</div>
<div class="stat-label">SATS LAST 24H</div>
</div>
</div>
<div class="panel-label" style="margin-top: 20px;">RECENT JOBS</div>
<div class="recent-jobs-list" id="recent-jobs-list">
<!-- Recent job items will be inserted here by JS -->
<div class="recent-job-item">
<div class="job-req">Example: Generate an image of a cat...</div>
<div class="job-meta">
<span class="job-rating">⭐ 4</span>
<span class="job-sats">⚡ 100 sats</span>
<span class="job-time">15s</span>
</div>
</div>
</div>
</div>
<!-- ── Session panel (left side) ─────────────────────────────────── -->
<div id="session-panel">
<button id="session-close"></button>
<h2>⚡ TIMMY — SESSION</h2>
<!-- Step: fund — choose deposit amount -->
<div data-session-step="fund">
<div class="panel-label">DEPOSIT AMOUNT (20010,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="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>
<!-- Step: invoice — pay the deposit -->
<div data-session-step="invoice" style="display:none">
<div class="panel-label">DEPOSIT AMOUNT</div>
<div class="amount-tag" id="session-invoice-amount">-- sats</div>
<div class="panel-label" style="margin-top:14px">SCAN OR COPY INVOICE</div>
<div class="qr-placeholder" id="session-invoice-qr">[ QR ]</div>
<div class="copy-row" style="margin-top:6px">
<div class="invoice-box" id="session-invoice-pr"></div>
<button class="copy-btn" onclick="_timmyCopy('session-invoice-pr')">COPY</button>
</div>
<span id="session-invoice-hash" data-hash="" style="display:none"></span>
<button class="panel-btn primary-green" id="session-pay-btn" style="margin-top:14px">⚡ SIMULATE PAYMENT</button>
<div id="session-status-invoice"></div>
</div>
<!-- Step: active — session running, show balance + topup -->
<div data-session-step="active" style="display:none">
<div class="panel-label">BALANCE</div>
<div class="amount-tag session-green" id="session-active-amount">-- sats</div>
<p style="font-size:10px;color:#1a4430;margin-top:14px;line-height:1.6;letter-spacing:1px">
TYPE IN THE INPUT BAR TO ASK TIMMY.<br>EACH REQUEST DEDUCTS FROM YOUR BALANCE.
</p>
<button class="panel-btn muted" id="session-topup-btn" style="margin-top:20px">⚡ TOP UP BALANCE</button>
<button id="session-clear-history-btn" class="session-link-btn">🗑 Clear history</button>
<div id="session-status-active"></div>
</div>
<!-- Step: topup — choose topup amount and pay -->
<div data-session-step="topup" style="display:none">
<div class="panel-label">TOPUP AMOUNT (20010,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="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">SCAN OR COPY INVOICE</div>
<div class="qr-placeholder" id="session-topup-qr">[ QR ]</div>
<div class="copy-row" style="margin-top:6px">
<div class="invoice-box" id="session-topup-pr"></div>
<button class="copy-btn" onclick="_timmyCopy('session-topup-pr')">COPY</button>
</div>
<span id="session-topup-hash" data-hash="" style="display:none"></span>
<button class="panel-btn primary-green" id="session-topup-pay-btn" style="margin-top:10px">⚡ SIMULATE TOPUP</button>
</div>
<div id="session-status-topup"></div>
<button class="panel-btn muted" id="session-back-btn" style="margin-top:10px">← BACK</button>
</div>
<div id="session-error"></div>
</div>
<!-- ── FPS crosshair ─────────────────────────────────────────────── -->
<div id="crosshair"></div>
<!-- ── Pointer-lock hint (desktop) ──────────────────────────────── -->
<div id="lock-hint">
<div class="lock-badge">CLICK TO ENTER · WASD TO MOVE · ESC TO EXIT</div>
</div>
<!-- ── Virtual joystick (mobile) ─────────────────────────────────── -->
<div id="joy-pad">
<div id="joy-nub"></div>
</div>
<!-- ── AR floating labels container ──────────────────────────────── -->
<div id="ar-labels" style="position:fixed;inset:0;pointer-events:none;z-index:15;overflow:hidden;"></div>
<div id="webgl-recovery-overlay">
<span class="recovery-text">GPU context lost — recovering...</span>
</div>
<script>
function _timmyCopy(elementId) {
const el = document.getElementById(elementId);
if (el) {
navigator.clipboard.writeText(el.textContent || el.innerText).then(() => {
// Optional: Add some visual feedback that text was copied
console.log('Copied to clipboard:', el.textContent);
}).catch(err => {
console.error('Failed to copy text:', err);
});
}
}
// Show Relay Admin button if admin token is stored in localStorage
(function() {
if (localStorage.getItem('relay_admin_token')) {
var btn = document.getElementById('relay-admin-btn');
if (btn) btn.style.display = 'block';
}
})();
</script>
<script type="module" src="./js/main.js"></script>
</body>
</html>