diff --git a/the-matrix/index.html b/the-matrix/index.html index eb03c35..b7a9951 100644 --- a/the-matrix/index.html +++ b/the-matrix/index.html @@ -495,6 +495,12 @@ 50% { opacity: 0.2; } } + /* ── Nostr identity prompt animation ─────────────────────────────── */ + @keyframes fadeInUp { + from { opacity: 0; transform: translateY(12px); } + to { opacity: 1; transform: translateY(0); } + } + /* ── Timmy identity card ──────────────────────────────────────────── */ #timmy-id-card { position: fixed; bottom: 80px; right: 16px; @@ -520,6 +526,7 @@

THE WORKSHOP

FPS: --
JOBS: 0
+
◌ AI: loading
Balance: -- sats ⚡ Top Up diff --git a/the-matrix/js/edge-worker-client.js b/the-matrix/js/edge-worker-client.js index eaa680f..0e5a01f 100644 --- a/the-matrix/js/edge-worker-client.js +++ b/the-matrix/js/edge-worker-client.js @@ -25,6 +25,7 @@ let _worker = null; let _ready = false; let _readyCb = null; +let _errorCb = null; const _pending = new Map(); // id → { resolve, reject } let _nextId = 1; @@ -48,6 +49,7 @@ function _init() { // Resolve all pending with fallback values for (const [, { resolve }] of _pending) resolve(_fallback(null)); _pending.clear(); + if (_errorCb) { _errorCb(data.message); _errorCb = null; } return; } @@ -103,6 +105,10 @@ export function onReady(fn) { _readyCb = fn; } +export function onError(fn) { + _errorCb = fn; +} + export function isReady() { return _ready; } /** diff --git a/the-matrix/js/main.js b/the-matrix/js/main.js index 3c25033..fc0ae51 100644 --- a/the-matrix/js/main.js +++ b/the-matrix/js/main.js @@ -12,8 +12,8 @@ import { initWebSocket, getConnectionState, getJobCount } from './websocket.js'; import { initPaymentPanel } from './payment.js'; import { initSessionPanel } from './session.js'; import { initNostrIdentity } from './nostr-identity.js'; -import { warmup as warmupEdgeWorker, onReady as onEdgeWorkerReady } from './edge-worker-client.js'; -import { setEdgeWorkerReady } from './ui.js'; +import { warmup as warmupEdgeWorker, onReady as onEdgeWorkerReady, onError as onEdgeWorkerError } from './edge-worker-client.js'; +import { setEdgeWorkerReady, setEdgeWorkerError } from './ui.js'; import { initTimmyId } from './timmy-id.js'; import { AGENT_DEFS } from './agent-defs.js'; import { initNavigation, updateNavigation, disposeNavigation } from './navigation.js'; @@ -49,6 +49,7 @@ function buildWorld(firstInit, stateSnapshot) { void initNostrIdentity('/api'); warmupEdgeWorker(); onEdgeWorkerReady(() => setEdgeWorkerReady()); + onEdgeWorkerError(() => setEdgeWorkerError()); void initTimmyId(); } diff --git a/the-matrix/js/ui.js b/the-matrix/js/ui.js index 5e47fbb..a477fd0 100644 --- a/the-matrix/js/ui.js +++ b/the-matrix/js/ui.js @@ -32,12 +32,34 @@ export function setInputBarSessionMode(active, placeholder) { } // ── Model-ready indicator ───────────────────────────────────────────────────── -// A small badge on the input bar showing when local AI is warm and ready. -// Hidden until the first `ready` event from the edge worker. +// 1. A small status line in the HUD: "◌ AI: loading" → "● AI: ready" / "✕ AI: error" +// 2. A badge on the input bar once the model is warm (subtle "⚡ local AI" cue). +// Both are updated by setEdgeWorkerReady() / setEdgeWorkerError(). let $readyBadge = null; +let _identityReady = false; + +function _updateEdgeHud(state) { + const $s = document.getElementById('edge-status'); + if (!$s) return; + if (state === 'ready') { + $s.textContent = '● AI: ready'; + $s.style.color = '#44cc88'; + $s.title = 'Local AI active — trivial queries answered without Lightning payment'; + } else if (state === 'error') { + $s.textContent = '✕ AI: error'; + $s.style.color = '#cc4444'; + $s.title = 'Local AI unavailable — all requests routed to server'; + } else { + $s.textContent = '◌ AI: loading'; + $s.style.color = ''; + $s.title = 'Local AI model loading…'; + } +} export function setEdgeWorkerReady() { + _updateEdgeHud('ready'); + if (!$readyBadge) { $readyBadge = document.createElement('span'); $readyBadge.id = 'edge-ready-badge'; @@ -58,6 +80,23 @@ export function setEdgeWorkerReady() { $readyBadge.style.display = ''; } +export function setEdgeWorkerError() { + _updateEdgeHud('error'); +} + +// Listen for Nostr identity resolved — update HUD tooltip to reflect combined state +window.addEventListener('nostr:identity-ready', (e) => { + _identityReady = true; + const pubkey = e.detail?.pubkey; + const $s = document.getElementById('edge-status'); + if ($s && $s.textContent.startsWith('●')) { + $s.title = `Local AI active · Nostr identity: ${pubkey ? pubkey.slice(0, 8) + '…' : 'connected'}`; + } + if ($readyBadge) { + $readyBadge.title = `Local AI + Nostr identity active${pubkey ? ' (' + pubkey.slice(0, 8) + '…)' : ''}`; + } +}); + // ── Cost preview badge ──────────────────────────────────────────────────────── // Shown beneath the input bar: "~N sats" / "FREE" / "answered locally". // Fetched from GET /api/estimate once the user stops typing (300 ms debounce). diff --git a/the-matrix/package.json b/the-matrix/package.json index 40b8266..c8d14cb 100644 --- a/the-matrix/package.json +++ b/the-matrix/package.json @@ -9,6 +9,7 @@ "preview": "vite preview" }, "dependencies": { + "@noble/hashes": "^1.7.2", "@xenova/transformers": "^2.17.2", "nostr-tools": "^2.23.3", "three": "0.171.0"