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 @@
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"