[claude] NIP-07 visitor identity in the workshop (#12) (#49)
Some checks failed
Deploy Nexus / deploy (push) Has been cancelled
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:
84
app.js
84
app.js
@@ -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;
|
||||
|
||||
11
index.html
11
index.html
@@ -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">
|
||||
|
||||
88
style.css
88
style.css
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user