[claude] NIP-07 visitor identity in the workshop (#12) (#49)
Some checks failed
Deploy Nexus / deploy (push) Has been cancelled

Co-authored-by: Claude (Opus 4.6) <claude@hermes.local>
Co-committed-by: Claude (Opus 4.6) <claude@hermes.local>
This commit was merged in pull request #49.
This commit is contained in:
2026-03-24 03:27:35 +00:00
committed by Timmy Time
parent a377da05de
commit db354e84f2
3 changed files with 182 additions and 1 deletions

84
app.js
View File

@@ -138,6 +138,7 @@ async function init() {
updateLoad(95);
setupControls();
initVisitorIdentity();
window.addEventListener('resize', onResize);
debugOverlay = document.getElementById('debug-overlay');
@@ -1189,12 +1190,93 @@ function addChatMessage(type, text) {
const container = document.getElementById('chat-messages');
const div = document.createElement('div');
div.className = `chat-msg chat-msg-${type}`;
const prefixes = { user: '[ALEXANDER]', timmy: '[TIMMY]', system: '[NEXUS]', error: '[ERROR]' };
let userPrefix = '[GUEST]';
if (type === 'user' && visitorPubkey) {
const short = visitorPubkey.slice(0, 8) + '…' + visitorPubkey.slice(-4);
userPrefix = `[${short}]`;
} else if (type === 'user') {
userPrefix = '[GUEST]';
}
const prefixes = { user: userPrefix, timmy: '[TIMMY]', system: '[NEXUS]', error: '[ERROR]' };
div.innerHTML = `<span class="chat-msg-prefix">${prefixes[type] || '[???]'}</span> ${text}`;
container.appendChild(div);
container.scrollTop = container.scrollHeight;
}
// ═══ NIP-07 VISITOR IDENTITY ═══
let visitorPubkey = null;
const VISITOR_STORAGE_KEY = 'nexus_visitor_pubkey';
function initVisitorIdentity() {
const cached = localStorage.getItem(VISITOR_STORAGE_KEY);
if (cached) {
visitorPubkey = cached;
updateVisitorUI(true);
addChatMessage('system', `Welcome back, ${abbrPubkey(cached)}. Identity recognized.`);
}
// Show connect button only if NIP-07 extension detected and not already connected
if (window.nostr && !visitorPubkey) {
document.getElementById('nostr-connect-btn').style.display = 'block';
}
document.getElementById('nostr-connect-btn').addEventListener('click', connectNostrIdentity);
document.getElementById('nostr-disconnect-btn').addEventListener('click', disconnectNostrIdentity);
}
async function connectNostrIdentity() {
if (!window.nostr) return;
const btn = document.getElementById('nostr-connect-btn');
btn.textContent = '⚡ CONNECTING…';
btn.disabled = true;
try {
const pubkey = await window.nostr.getPublicKey();
if (!pubkey) throw new Error('No pubkey returned');
visitorPubkey = pubkey;
localStorage.setItem(VISITOR_STORAGE_KEY, pubkey);
updateVisitorUI(true);
addChatMessage('system', `Identity linked: ${abbrPubkey(pubkey)}. Timmy remembers you.`);
} catch (e) {
btn.textContent = '⚡ CONNECT IDENTITY';
btn.disabled = false;
addChatMessage('error', 'Identity connection failed. Extension declined or unavailable.');
}
}
function disconnectNostrIdentity() {
visitorPubkey = null;
localStorage.removeItem(VISITOR_STORAGE_KEY);
updateVisitorUI(false);
addChatMessage('system', 'Identity disconnected. Returning to guest mode.');
if (window.nostr) {
document.getElementById('nostr-connect-btn').style.display = 'block';
}
}
function updateVisitorUI(connected) {
const indicator = document.getElementById('visitor-indicator');
const nameEl = document.getElementById('visitor-name');
const connectBtn = document.getElementById('nostr-connect-btn');
const disconnectBtn = document.getElementById('nostr-disconnect-btn');
if (connected && visitorPubkey) {
indicator.className = 'visitor-indicator connected';
nameEl.className = 'connected';
nameEl.textContent = abbrPubkey(visitorPubkey);
connectBtn.style.display = 'none';
disconnectBtn.style.display = 'block';
} else {
indicator.className = 'visitor-indicator';
nameEl.className = '';
nameEl.textContent = 'GUEST';
disconnectBtn.style.display = 'none';
}
}
function abbrPubkey(pubkey) {
return pubkey.slice(0, 8) + '…' + pubkey.slice(-4);
}
// ═══ PORTAL INTERACTION ═══
function checkPortalProximity() {
if (portalOverlayActive) return;

View File

@@ -80,6 +80,17 @@
<div id="agent-log-content" class="agent-log-content"></div>
</div>
<!-- Bottom Right: Visitor Identity -->
<div id="visitor-identity" class="visitor-identity-panel">
<div class="visitor-identity-label">VISITOR</div>
<div class="visitor-identity-status">
<span id="visitor-indicator" class="visitor-indicator"></span>
<span id="visitor-name">GUEST</span>
</div>
<button id="nostr-connect-btn" class="nostr-connect-btn" style="display:none;">⚡ CONNECT IDENTITY</button>
<button id="nostr-disconnect-btn" class="nostr-disconnect-btn" style="display:none;">DISCONNECT</button>
</div>
<!-- Bottom: Chat Interface -->
<div id="chat-panel" class="chat-panel">
<div class="chat-header">

View File

@@ -625,6 +625,94 @@ canvas#nexus-canvas {
color: var(--color-primary);
}
/* Visitor Identity Panel */
.visitor-identity-panel {
position: absolute;
bottom: 80px;
right: var(--space-3);
width: 200px;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(8px);
border-left: 2px solid var(--color-secondary);
padding: var(--space-2) var(--space-3);
font-size: var(--text-xs);
pointer-events: auto;
}
.visitor-identity-label {
font-family: var(--font-display);
color: var(--color-secondary);
letter-spacing: 0.1em;
margin-bottom: var(--space-1);
opacity: 0.8;
font-size: 10px;
}
.visitor-identity-status {
display: flex;
align-items: center;
gap: var(--space-2);
margin-bottom: var(--space-2);
}
.visitor-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--color-text-muted);
flex-shrink: 0;
}
.visitor-indicator.connected {
background: var(--color-primary);
box-shadow: 0 0 6px rgba(74, 240, 192, 0.6);
animation: visitor-pulse 2s ease-in-out infinite;
}
@keyframes visitor-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
#visitor-name {
color: var(--color-text);
font-family: var(--font-body);
font-size: var(--text-xs);
word-break: break-all;
}
#visitor-name.connected {
color: var(--color-primary);
font-weight: 600;
}
.nostr-connect-btn {
width: 100%;
background: transparent;
border: 1px solid var(--color-secondary);
color: var(--color-secondary);
font-family: var(--font-body);
font-size: 10px;
padding: var(--space-1) var(--space-2);
cursor: pointer;
letter-spacing: 0.05em;
border-radius: 2px;
transition: background var(--transition-ui), color var(--transition-ui);
}
.nostr-connect-btn:hover {
background: var(--color-secondary);
color: var(--color-bg);
}
.nostr-disconnect-btn {
width: 100%;
background: transparent;
border: 1px solid var(--color-text-muted);
color: var(--color-text-muted);
font-family: var(--font-body);
font-size: 10px;
padding: var(--space-1) var(--space-2);
cursor: pointer;
letter-spacing: 0.05em;
border-radius: 2px;
transition: border-color var(--transition-ui), color var(--transition-ui);
}
.nostr-disconnect-btn:hover {
border-color: var(--color-danger);
color: var(--color-danger);
}
/* Mobile adjustments */
@media (max-width: 480px) {
.chat-panel {