Files
timmy-tower/the-matrix/js/edge-worker-client.js
Replit Agent 494393017c task-28 fix3: complexity contract, consistent token headers, npub-only prompt
1. edge-worker.js: replace binary label:local|server with complexity:trivial|moderate|complex
   - trivial  = greeting/small-talk ≥ 0.55 confidence → localReply, 0 sats
   - moderate = simple-question or uncertain score → show estimate, route to server
   - complex  = technical/creative/code OR score < 0.40 → show estimate, route to server
   - model-unavailable fallback → moderate (safe default, not 'server')

2. edge-worker-client.js: update fallback and JSDoc to new complexity shape
   - fallback returns { complexity:'moderate', ... } instead of { label:'server', ... }

3. ui.js: triage driven by cls.complexity, not cls.label
   - trivial + localReply → local answer, 0 sats badge, no server call
   - moderate/complex → _fetchEstimate() fired on classify outcome (not just debounce)
     then routed to server via WebSocket

4. session.js: X-Nostr-Token attached consistently on ALL outbound session calls
   - _startDepositPolling: GET /sessions/:id now includes X-Nostr-Token header
   - _startTopupPolling: GET /sessions/:id now includes X-Nostr-Token header
   - _tryRestore: GET /sessions/:id now includes X-Nostr-Token header
   - _createTopup: POST /sessions/:id/topup now includes X-Nostr-Token header

5. nostr-identity.js: _canSign flag tracks signing capability separately from pubkey
   - initNostrIdentity sets _canSign=true only when NIP-07 or privkey is available
   - npub-only discovery sets _pubkey but _canSign=false → prompt IS scheduled
   - Prompt shown when !_pubkey || !_canSign (not just !_pubkey)
   - Prompt click handlers set _canSign=true after connecting NIP-07 or generating key
   - refreshToken only called when _pubkey && _canSign (avoids silent failures)
2026-03-19 19:02:45 +00:00

113 lines
3.8 KiB
JavaScript

/**
* edge-worker-client.js — Main-thread proxy for the edge-worker Web Worker.
*
* Spawns js/edge-worker.js as a module Worker and exposes:
* classify(text) → Promise<{
* complexity: 'trivial'|'moderate'|'complex',
* score: number,
* reason: string,
* localReply?: string // only when complexity === 'trivial'
* }>
* sentiment(text) → Promise<{ label:'POSITIVE'|'NEGATIVE'|'NEUTRAL', score }>
* onReady(fn) → register a callback fired when models finish loading
* isReady() → boolean — true once both models are warm
* warmup() → start the worker early so first classify() is fast
*
* Complexity tiers (set by the worker):
* trivial — greeting/small-talk; answered locally, 0 sats, no server call
* moderate — simple question; show cost preview, route to server
* complex — technical/creative/code; always priced, show cost preview
*
* If Web Workers are unavailable (SSR / old browser), all calls fall back
* gracefully: classify → { complexity:'moderate', ... } so the app still works.
*/
let _worker = null;
let _ready = false;
let _readyCb = null;
const _pending = new Map(); // id → { resolve, reject }
let _nextId = 1;
function _init() {
if (_worker) return;
try {
// Use import.meta.url so Vite can resolve the worker URL correctly.
// type:'module' is required for ESM imports inside the worker.
_worker = new Worker(new URL('./edge-worker.js', import.meta.url), { type: 'module' });
_worker.addEventListener('message', ({ data }) => {
// Lifecycle events have no id
if (data?.type === 'ready') {
_ready = true;
if (_readyCb) { _readyCb(); _readyCb = null; }
return;
}
if (data?.type === 'error') {
console.warn('[edge-worker] worker boot error:', data.message);
// Resolve all pending with fallback values
for (const [, { resolve }] of _pending) resolve(_fallback(null));
_pending.clear();
return;
}
// Regular response: { id, result }
const { id, result } = data ?? {};
const entry = _pending.get(id);
if (entry) {
_pending.delete(id);
entry.resolve(result);
}
});
_worker.addEventListener('error', (err) => {
console.warn('[edge-worker] worker error:', err.message);
});
} catch (err) {
console.warn('[edge-worker] Web Workers unavailable — using fallback routing:', err.message);
_worker = null;
}
}
function _fallback(type) {
if (type === 'sentiment') return { label: 'NEUTRAL', score: 0.5 };
// classify fallback: moderate keeps the UI functional (shows estimate, routes to server)
return { complexity: 'moderate', score: 0, reason: 'worker-unavailable' };
}
function _send(type, text) {
if (!_worker) return Promise.resolve(_fallback(type));
const id = _nextId++;
return new Promise((resolve) => {
_pending.set(id, { resolve, reject: resolve });
_worker.postMessage({ id, type, text });
});
}
// ── Public API ────────────────────────────────────────────────────────────────
export function classify(text) {
_init();
return _send('classify', text);
}
export function sentiment(text) {
_init();
return _send('sentiment', text);
}
export function onReady(fn) {
if (_ready) { fn(); return; }
_readyCb = fn;
}
export function isReady() { return _ready; }
/**
* warmup() — start the worker (and model loading) early so classify/sentiment
* calls on first user interaction don't stall waiting for models.
*/
export function warmup() { _init(); }