josephrw's picture
Upload folder using huggingface_hub
c4916b2 verified
Raw
History Blame Contribute Delete
7.4 kB
let socket = null;
let sessionId = null;
let workerId = null;
let token = null;
let heartbeatInterval = null;
let deviceKeys = null;
function log(msg) {
const el = document.getElementById('log');
const line = document.createElement('div');
line.textContent = new Date().toLocaleTimeString() + ' ' + msg;
el.appendChild(line);
el.scrollTop = el.scrollHeight;
console.log(msg);
}
function parseSessionFromUrl() {
const params = new URLSearchParams(window.location.search);
sessionId = params.get('session_id');
token = params.get('token');
return { sessionId, token };
}
async function connectToSpace() {
parseSessionFromUrl();
if (!sessionId || !token) {
log('No session_id or token in URL');
return;
}
workerId = 'wk_' + Math.random().toString(36).slice(2, 10);
log('Connecting as ' + workerId + ' to session ' + sessionId);
const wsUrl = (location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ws/worker/' + sessionId + '/' + workerId;
socket = new WebSocket(wsUrl);
socket.onopen = async () => {
log('WebSocket connected');
updateStatus('connecting');
deviceKeys = await ReceiptCrypto.loadDeviceKeypair();
const runtime = PhoneRuntime.selectBestRuntime();
sendWorkerHello(runtime);
};
socket.onmessage = (event) => {
const msg = JSON.parse(event.data);
handleSocketMessage(msg);
};
socket.onclose = () => {
log('WebSocket closed');
updateStatus('offline');
stopHeartbeat();
};
socket.onerror = (err) => {
log('WebSocket error');
updateStatus('offline');
};
document.getElementById('btnConnect').disabled = true;
document.getElementById('btnDisconnect').disabled = false;
}
function sendWorkerHello(runtimeType) {
if (!socket) return;
socket.send(JSON.stringify({
op: 'worker_hello',
worker_id: workerId,
runtime_type: runtimeType,
device_public_key: deviceKeys ? deviceKeys.publicKey : null,
}));
}
function detectDeviceInfo() {
const battery = navigator.getBattery ? navigator.getBattery() : Promise.resolve({});
battery.then(b => {
document.getElementById('batteryValue').textContent = (b.level ? Math.round(b.level * 100) + '%' : 'unknown');
}).catch(() => {});
document.getElementById('networkValue').textContent = navigator.connection ? navigator.connection.effectiveType : 'unknown';
document.getElementById('thermalValue').textContent = 'nominal'; // iOS Web API limitation
document.getElementById('runtimeValue').textContent = PhoneRuntime.selectBestRuntime();
}
function advertiseCapabilities() {
if (!socket) return;
const caps = [
{ capability_name: 'iphone.text.embedding.private', runtime_type: PhoneRuntime.selectBestRuntime(), model_id: 'mock-embedding' },
{ capability_name: 'iphone.image.classify.local', runtime_type: PhoneRuntime.selectBestRuntime(), model_id: 'mock-image' },
{ capability_name: 'iphone.privacy.redact.local', runtime_type: PhoneRuntime.selectBestRuntime(), model_id: 'mock-redact' },
];
socket.send(JSON.stringify({ op: 'capabilities', worker_id: workerId, capabilities: caps }));
renderCapabilities(caps);
log('Capabilities advertised');
}
function handleSocketMessage(msg) {
switch (msg.op) {
case 'worker_welcome':
updateStatus('online');
detectDeviceInfo();
advertiseCapabilities();
startHeartbeat();
break;
case 'job_offer':
handleJobOffer(msg);
break;
case 'session_update':
log('Session update: ' + JSON.stringify(msg));
break;
case 'error':
log('Server error: ' + msg.error);
break;
}
}
async function handleJobOffer(job) {
log('Job offered: ' + job.job_type + ' (' + job.job_id + ')');
// Auto-accept for v1
socket.send(JSON.stringify({ op: 'job_accept', job_id: job.job_id, worker_id: workerId }));
const startTime = Date.now();
let output = {};
try {
switch (job.job_type) {
case 'text_embedding':
output = await PhoneRuntime.runTextEmbedding(job.payload.text || '');
break;
case 'image_classification':
output = await PhoneRuntime.runImageClassification(job.payload.image || '');
break;
case 'privacy_redaction':
output = await PhoneRuntime.runPrivacyRedaction(job.payload.text || '');
break;
default:
throw new Error('Unsupported job type: ' + job.job_type);
}
} catch (err) {
log('Job failed: ' + err.message);
socket.send(JSON.stringify({ op: 'job_result', job_id: job.job_id, worker_id: workerId, output: { error: err.message }, latency_ms: Date.now() - startTime }));
return;
}
const latencyMs = Date.now() - startTime;
const inputHash = await ReceiptCrypto.makeInputHash(job.payload);
const outputHash = await ReceiptCrypto.makeOutputHash(output);
const deviceSignature = await ReceiptCrypto.signReceipt({
job_id: job.job_id,
worker_id: workerId,
input_hash: inputHash,
output_hash: outputHash,
latency_ms: latencyMs,
});
socket.send(JSON.stringify({
op: 'job_result',
job_id: job.job_id,
worker_id: workerId,
output: output,
latency_ms: latencyMs,
input_hash: inputHash,
output_hash: outputHash,
device_signature: deviceSignature,
}));
addReceiptCard(job.job_id, job.job_type, latencyMs, outputHash);
log('Job completed: ' + job.job_id + ' in ' + latencyMs + 'ms');
}
function startHeartbeat() {
heartbeatInterval = setInterval(() => {
if (!socket || socket.readyState !== WebSocket.OPEN) return;
socket.send(JSON.stringify({
op: 'heartbeat',
worker_id: workerId,
battery_level: 0.8, // mock for v1
thermal_state: 'nominal',
jobs_completed: document.querySelectorAll('.receipt-card').length,
}));
}, 5000);
}
function stopHeartbeat() {
if (heartbeatInterval) clearInterval(heartbeatInterval);
heartbeatInterval = null;
}
function disconnectWorker() {
stopHeartbeat();
if (socket) {
socket.send(JSON.stringify({ op: 'disconnect', worker_id: workerId, reason: 'user' }));
socket.close();
socket = null;
}
updateStatus('offline');
document.getElementById('btnConnect').disabled = false;
document.getElementById('btnDisconnect').disabled = true;
}
function updateStatus(status) {
const badge = document.getElementById('statusBadge');
badge.className = 'status-badge status-' + status;
badge.textContent = status.charAt(0).toUpperCase() + status.slice(1);
}
function renderCapabilities(caps) {
const el = document.getElementById('capabilitiesList');
el.innerHTML = caps.map(c =>
'<div class="job-card"><div class="job-type">' + c.capability_name + '</div><div style="font-size:12px;color:#64748b">' + c.runtime_type + ' / ' + c.model_id + '</div></div>'
).join('');
}
function addReceiptCard(jobId, jobType, latencyMs, outputHash) {
const el = document.getElementById('receiptLog');
const card = document.createElement('div');
card.className = 'receipt-card';
card.innerHTML = '<div style="font-weight:700;margin-bottom:4px;">' + jobType + '</div><div>Latency: ' + latencyMs + 'ms</div><div style="color:#059669">' + outputHash.slice(0, 24) + '...</div>';
el.prepend(card);
}
// Auto-connect if URL has session params
if (window.location.search.includes('session_id')) {
window.addEventListener('load', () => {
setTimeout(connectToSpace, 500);
});
}