Spaces:
Sleeping
Sleeping
| // DaisyChain-Infer client: connect P2P, take a slice of a model straight from | |
| // the Hub, and run one token loop around the ring. | |
| // | |
| // The mesh (signaling, WebRTC, fragmentation, rooms, reconnect) is | |
| // DaisyChain-Web's, because those are the parts already proven against real | |
| // phones and real NATs. Everything from "the model" down is new. | |
| ; | |
| const STUN = [ | |
| { urls: "stun:stun.l.google.com:19302" }, | |
| { urls: "turn:openrelay.metered.ca:80", username: "openrelayproject", credential: "openrelayproject" }, | |
| { urls: "turn:openrelay.metered.ca:443", username: "openrelayproject", credential: "openrelayproject" }, | |
| { urls: "turns:openrelay.metered.ca:443?transport=tcp", username: "openrelayproject", credential: "openrelayproject" }, | |
| ]; | |
| let rtcConfig = { iceServers: STUN }; | |
| const ui = {}; | |
| for (const id of ["status", "backend", "me", "peers", "log", "requests", "roomInfo", "roomCode", | |
| "copyLink", "model", "tokenizer", "plan", "myShard", "prompt", "genBtn", | |
| "stopBtn", "out", "tps", "tokcount", "hops", "temp", "vtemp", "ntok", "vntok", | |
| "seed", "verify", "verifyRow", "ringState", "repo", "revision", "loadRepo", | |
| "modelState", "ctxLen", "vctxLen", "tokenState", "clearToken", | |
| "tokModal", "tokInput", "tokOk", "tokCancel", "tokWhy", | |
| "tokPasteRow", "tokSignIn", "tokSignInBtn", | |
| "lobby", "createRoom", "joinRoom", "joinCode"]) | |
| ui[id] = document.getElementById(id); | |
| function log(m) { ui.log.textContent = `${new Date().toLocaleTimeString()} ${redact(m)}\n` + ui.log.textContent; } | |
| function setStatus(s) { ui.status.textContent = s; } | |
| // ---- the token -------------------------------------------------------------- | |
| // Held in a closure variable and nowhere else. Not localStorage, not | |
| // sessionStorage, not a cookie, not the URL, not the log, and never on the | |
| // wire — each device authenticates itself, so a peer never needs anyone | |
| // else's credentials. Reloading the page is meant to lose it. | |
| let hfToken = null, tokenSource = ""; | |
| let oauthAvailable = false; | |
| function redact(s) { return hfToken ? String(s).split(hfToken).join("hf_***") : String(s); } | |
| function haveToken() { return !!hfToken; } | |
| function setToken(t, source) { | |
| hfToken = t || null; | |
| tokenSource = t ? (source || "entered") : ""; | |
| ui.tokenState.textContent = hfToken | |
| ? `${tokenSource} · held in memory for this tab only` | |
| : (oauthAvailable ? "not signed in" : "none"); | |
| ui.clearToken.style.display = hfToken ? "" : "none"; | |
| } | |
| function clearToken() { setToken(null); log("token cleared from memory"); } | |
| // A hosted deployment hands the OAuth token back in the URL *fragment*, which | |
| // browsers never send to a server. Take it, then strip it from the address bar | |
| // and from history immediately — a credential sitting in a visible URL is one | |
| // screenshot away from being shared. | |
| function adoptTokenFromFragment() { | |
| if (!location.hash || location.hash.length < 2) return; | |
| const h = new URLSearchParams(location.hash.slice(1)); | |
| const t = h.get("hf"), err = h.get("oauth_error"); | |
| if (t || err) history.replaceState(null, "", location.pathname + location.search); | |
| if (err) { log(`sign-in failed: ${err}`); return; } | |
| if (t) { setToken(t, "signed in with Hugging Face"); log("signed in with Hugging Face — token held in memory for this tab only"); } | |
| } | |
| // One-time prompt. Resolves to a token string, or null if the user declines. | |
| // Deliberately a modal rather than a stored setting: the moment a credential | |
| // becomes ambient, it becomes something you forget you granted. | |
| // Where OAuth is available — a deployment the user does not control — the | |
| // paste box is not offered at all. Asking someone to type a personal access | |
| // token into a page served by a third party is a bad pattern even when the | |
| // code is honest, because the user cannot verify that it is. Signing in hands | |
| // over a scoped, expiring token instead, and it is the only path shown there. | |
| let tokenPrompt = null; | |
| function requestToken(why) { | |
| if (tokenPrompt) return tokenPrompt; | |
| ui.tokWhy.textContent = why || "This model needs a Hugging Face token to download."; | |
| ui.tokPasteRow.style.display = oauthAvailable ? "none" : ""; | |
| ui.tokSignIn.style.display = oauthAvailable ? "" : "none"; | |
| ui.tokOk.style.display = oauthAvailable ? "none" : ""; // nothing to submit | |
| ui.tokInput.value = ""; | |
| ui.tokModal.style.display = "flex"; | |
| if (!oauthAvailable) ui.tokInput.focus(); | |
| tokenPrompt = new Promise((resolve) => { | |
| const done = (val) => { | |
| ui.tokModal.style.display = "none"; | |
| ui.tokInput.value = ""; // do not leave it in the DOM | |
| ui.tokOk.onclick = null; ui.tokCancel.onclick = null; | |
| ui.tokInput.onkeydown = null; ui.tokSignInBtn.onclick = null; | |
| tokenPrompt = null; | |
| resolve(val); | |
| }; | |
| ui.tokSignInBtn.onclick = () => { | |
| // full-page redirect: we come back with the token in the fragment | |
| log("redirecting to Hugging Face to sign in…"); | |
| location.href = "auth/login"; | |
| }; | |
| ui.tokOk.onclick = () => { | |
| const v = ui.tokInput.value.trim(); | |
| if (!v) return done(null); | |
| setToken(v, "entered by hand"); | |
| log("token accepted — kept in memory for this tab only, never sent to peers"); | |
| done(v); | |
| }; | |
| ui.tokCancel.onclick = () => { log("token request declined"); done(null); }; | |
| ui.tokInput.onkeydown = (e) => { if (e.key === "Enter") ui.tokOk.onclick(); if (e.key === "Escape") ui.tokCancel.onclick(); }; | |
| }); | |
| return tokenPrompt; | |
| } | |
| // Run an HF call, and if it fails for want of credentials ask once, then retry. | |
| async function withAuth(fn, what) { | |
| try { return await fn(hfToken); } | |
| catch (e) { | |
| if (e && e.kind === "auth") { | |
| const gated = e.status === 403 ? " It is gated or private" : ""; | |
| const t = await requestToken(oauthAvailable | |
| ? `${what} needs Hugging Face access.${gated}${gated ? " — for a gated model, accept its licence on the model page first." : ""}` | |
| : `${what} needs a Hugging Face token.${gated}. Create a READ token at huggingface.co/settings/tokens.`); | |
| if (!t) throw new Error(`${what}: cancelled — no token provided`); | |
| return fn(t); | |
| } | |
| throw e; | |
| } | |
| } | |
| // ---- identity --------------------------------------------------------------- | |
| const ADJ = ["Frosted", "Silver", "Pale", "Winter", "Hollow", "Quiet", "Drifting", "Little", | |
| "Glacier", "Misty", "Northern", "Still", "Brave", "Snowlit", "Amber"]; | |
| const NOUN = ["Fox", "Hare", "Owl", "Elk", "Marten", "Sparrow", "Otter", "Deer", | |
| "Ptarmigan", "Pine", "Birch", "Wren", "Fawn", "Moth", "Lynx"]; | |
| const deviceName = ADJ[Math.floor(Math.random() * ADJ.length)] + " " + | |
| NOUN[Math.floor(Math.random() * NOUN.length)]; | |
| let myId = null, compute = null, ws = null, L = null, wasDenied = false; | |
| const pcs = new Map(), chans = new Map(), names = new Map(); | |
| const caps = new Map(); | |
| // ---- model state ------------------------------------------------------------ | |
| let repoInfo = null; // {repo, revision, spec, tensors, fingerprint} — head only | |
| let spec = null, plan = null, myStage = null, stageObj = null; | |
| let tok = null; // tokenizer, loaded from the same repo | |
| let modelHash = 0; | |
| let running = false, abortRun = false; | |
| let probeHash = 0, auditFailure = null; | |
| const readyStages = new Set(); | |
| function nmeOf(id) { return names.get(id) || id; } | |
| function room() { return new URLSearchParams(location.search).get("room"); } | |
| // ?relay=1 skips WebRTC entirely and routes through the server. It exists as a | |
| // diagnostic: if a ring works with it and not without it, the problem is NAT | |
| // traversal, not this code. | |
| const forceRelay = new URLSearchParams(location.search).get("relay") === "1"; | |
| function updatePeers() { | |
| if (!names.size) { ui.peers.textContent = "(none yet — you can still run solo)"; return; } | |
| ui.peers.textContent = [...names.entries()].map(([id, n]) => | |
| `${n} ${chans.has(id) ? (relayed.has(id) ? "⇄ via server" : "✓") : "(connecting…)"}`).join(", "); | |
| } | |
| function ctxLen() { return +ui.ctxLen.value; } | |
| // ---- signaling + WebRTC ----------------------------------------------------- | |
| function connectSignaling() { | |
| const proto = location.protocol === "https:" ? "wss" : "ws"; | |
| const params = new URLSearchParams(); | |
| params.set("name", deviceName); | |
| if (room()) params.set("room", room()); | |
| ws = new WebSocket(`${proto}://${location.host}/?${params}`); | |
| ws.onopen = () => setStatus(room() ? `connected — private room "${room()}"` : "connected — grouping with devices on your network"); | |
| ws.onclose = () => { | |
| if (wasDenied) return; | |
| setStatus("signaling disconnected — reconnecting…"); | |
| setTimeout(connectSignaling, 3000); | |
| }; | |
| ws.onmessage = async (ev) => { | |
| const msg = JSON.parse(ev.data); | |
| if (msg.type === "welcome") { | |
| myId = msg.id; | |
| if (msg.room) log(`group: ${msg.room.startsWith("net:") ? "your network" : "private room " + msg.room.replace("room:", "")}`); | |
| if (msg.host) { log("you host this room — joiners wait for your approval"); setStatus(`hosting private room "${room()}"`); } | |
| else if (room()) setStatus(`accepted into private room "${room()}"`); | |
| for (const p of msg.peers) { names.set(p.id, p.name); if (!chans.has(p.id)) initiatePeer(p.id); } | |
| updatePeers(); | |
| } else if (msg.type === "waiting") { | |
| setStatus("knocking — waiting for the room's host to let you in…"); | |
| } else if (msg.type === "denied") { | |
| wasDenied = true; setStatus("the host declined your request to join"); log("join request declined by the host"); | |
| } else if (msg.type === "host") { | |
| log("the host left — you are now the host of this room"); setStatus(`hosting private room "${room()}"`); | |
| } else if (msg.type === "join-request") { | |
| addJoinRequest(msg.id, msg.name); | |
| } else if (msg.type === "peer-joined") { | |
| names.set(msg.id, msg.name); updatePeers(); log(`${msg.name} joined`); | |
| // Only the NEWEST peer dials (so the two never collide with competing | |
| // offers), which means this side is waiting to be called. It still needs | |
| // a deadline: if the offer never arrives, or arrives and its ICE never | |
| // completes, there is no connection object here to fail — and without a | |
| // watchdog this side shows "(connecting…)" forever with nothing to | |
| // recover from. That is the failure two separate machines actually hit. | |
| if (forceRelay) useRelay(msg.id); | |
| else if (!chans.has(msg.id)) armWatchdog(msg.id); | |
| } else if (msg.type === "peer-left") { | |
| log(`${nmeOf(msg.id)} left`); cleanupPeer(msg.id); names.delete(msg.id); caps.delete(msg.id); updatePeers(); renderPlan(); | |
| } else if (msg.type === "signal") { | |
| await onSignal(msg.from, msg.data); | |
| } else if (msg.type === "ping") { | |
| ws.send(JSON.stringify({ type: "pong" })); | |
| } else if (msg.type === "relay") { | |
| onWire(msg.from, bufFromB64(msg.data)); | |
| } | |
| }; | |
| } | |
| function signal(to, data) { ws.send(JSON.stringify({ type: "signal", to, data })); } | |
| function addJoinRequest(id, name) { | |
| const row = document.createElement("div"); | |
| row.className = "joinrow"; | |
| const who = document.createElement("span"); | |
| who.textContent = `❄ ${name} wants to join`; // textContent: names come off the wire | |
| const btn = (label, allow) => { | |
| const b = document.createElement("button"); | |
| b.textContent = label; | |
| b.className = allow ? "small" : "small danger"; | |
| b.onclick = () => { | |
| ws.send(JSON.stringify({ type: "admit", id, allow })); | |
| row.remove(); | |
| log(allow ? `you let ${name} in` : `you declined ${name}`); | |
| }; | |
| return b; | |
| }; | |
| row.append(who, btn("Accept", true), btn("Deny", false)); | |
| ui.requests.appendChild(row); | |
| } | |
| // ---- connection watchdog ----------------------------------------------------- | |
| // WebRTC can fail by never finishing. A peer whose ICE never completes sits in | |
| // "checking" indefinitely: `dc.onclose` never fires because the channel never | |
| // opened, and `connectionState` may never reach "failed" either. Without a | |
| // timeout the UI shows "(connecting…)" forever, the relay fallback below is | |
| // never reached, and nothing says why — which is exactly what happened on two | |
| // real machines while two tabs on one machine worked fine, because same-host | |
| // candidates always succeed. | |
| // | |
| // So: give each attempt a deadline, say what state it died in, retry once, then | |
| // fall back to relaying through the server. | |
| const CONNECT_TIMEOUT = 12000; | |
| const MAX_DIRECT_ATTEMPTS = 2; | |
| const watchdogs = new Map(); // peerId -> timer | |
| const attempts = new Map(); // peerId -> count | |
| const relayed = new Set(); // peers we reach via the server | |
| function clearWatchdog(peerId) { | |
| const t = watchdogs.get(peerId); | |
| if (t) { clearTimeout(t); watchdogs.delete(peerId); } | |
| } | |
| function armWatchdog(peerId) { | |
| clearWatchdog(peerId); | |
| watchdogs.set(peerId, setTimeout(() => { | |
| watchdogs.delete(peerId); | |
| if (chans.has(peerId) || !names.has(peerId)) return; | |
| const pc = pcs.get(peerId); | |
| const n = (attempts.get(peerId) || 0) + 1; | |
| attempts.set(peerId, n); | |
| log(`no direct path to ${nmeOf(peerId)} after ${CONNECT_TIMEOUT / 1000}s — ` + | |
| `ICE ${pc ? pc.iceConnectionState : "?"}, gathering ${pc ? pc.iceGatheringState : "?"} ` + | |
| `(attempt ${n} of ${MAX_DIRECT_ATTEMPTS})`); | |
| if (n < MAX_DIRECT_ATTEMPTS) { | |
| // one side redials so the two do not collide (offer glare); the other | |
| // just waits out another deadline | |
| if (+myId.slice(1) > +peerId.slice(1)) { cleanupPeer(peerId); initiatePeer(peerId); } | |
| else armWatchdog(peerId); | |
| return; | |
| } | |
| useRelay(peerId); // symmetric: both sides install it | |
| }, CONNECT_TIMEOUT)); | |
| } | |
| // Relay through the signaling server. Both peers already hold a WebSocket to | |
| // it, so this is a path that cannot fail for NAT reasons — but it means the | |
| // server carries activations, which the direct path specifically avoids. That | |
| // is a real change in what the server sees, so it is stated loudly rather than | |
| // slipped in as a silent recovery. | |
| function useRelay(peerId) { | |
| if (chans.has(peerId) || !names.has(peerId)) return; | |
| cleanupPeer(peerId); | |
| chans.set(peerId, makeRelayChannel(peerId)); | |
| relayed.add(peerId); | |
| log(`⚠ falling back to the SERVER RELAY for ${nmeOf(peerId)} — no direct WebRTC path could be ` + | |
| `established. This works, but activations for that hop now pass through the server instead ` + | |
| `of peer-to-peer. Supply a TURN server (DAISY_RTC_CONFIG) for a direct path.`); | |
| // A relay channel is a plain object with no "open" event, so nothing here | |
| // fires dc.onopen — and without this the capability exchange never happens, | |
| // the peer never enters the plan, and the ring silently shrinks to whoever | |
| // had a direct channel. Losing a stage quietly is worse than failing loudly. | |
| sendHello(peerId); | |
| updatePeers(); wake(); | |
| } | |
| function newPC(peerId) { | |
| const pc = new RTCPeerConnection(rtcConfig); | |
| pc.onicecandidate = (e) => { if (e.candidate) signal(peerId, { candidate: e.candidate }); }; | |
| // Terse but real diagnostics: without these, a cross-machine failure is | |
| // indistinguishable from a hang. | |
| pc.oniceconnectionstatechange = () => { | |
| const s = pc.iceConnectionState; | |
| if (s === "failed" || s === "disconnected" || s === "connected" || s === "completed") | |
| log(`${nmeOf(peerId)}: ICE ${s}`); | |
| }; | |
| pc.onicegatheringstatechange = () => { | |
| if (pc.iceGatheringState === "complete" && !chans.has(peerId)) | |
| log(`${nmeOf(peerId)}: finished gathering candidates, still no channel`); | |
| }; | |
| pc.onconnectionstatechange = () => { | |
| if (pc.connectionState === "failed") { | |
| log(`${nmeOf(peerId)}: connection failed`); | |
| clearWatchdog(peerId); | |
| const n = (attempts.get(peerId) || 0) + 1; | |
| attempts.set(peerId, n); | |
| cleanupPeer(peerId); | |
| if (n < MAX_DIRECT_ATTEMPTS) scheduleReconnect(peerId); | |
| else useRelay(peerId); | |
| } | |
| if (pc.connectionState === "disconnected") | |
| setTimeout(() => { | |
| if (pcs.get(peerId) === pc && pc.connectionState === "disconnected") { | |
| log(`${nmeOf(peerId)} connection did not recover — redialing`); | |
| cleanupPeer(peerId); scheduleReconnect(peerId); | |
| } | |
| }, 4000); | |
| }; | |
| pcs.set(peerId, pc); | |
| return pc; | |
| } | |
| // Which path actually carried the connection — host, srflx (STUN) or relay | |
| // (TURN). Printed on success because "it connected" and "it connected the way | |
| // you think" are different facts. | |
| async function logSelectedPath(peerId, pc) { | |
| try { | |
| const stats = await pc.getStats(); | |
| let pair = null; | |
| stats.forEach(r => { | |
| if (r.type === "candidate-pair" && r.state === "succeeded" && (r.selected || r.nominated)) pair = r; | |
| }); | |
| if (!pair) return; | |
| const loc = stats.get(pair.localCandidateId), rem = stats.get(pair.remoteCandidateId); | |
| log(`${nmeOf(peerId)}: direct path via ${loc ? loc.candidateType : "?"} → ${rem ? rem.candidateType : "?"}`); | |
| } catch (e) {} | |
| } | |
| function b64FromBuf(buf) { | |
| const u = new Uint8Array(buf); let s = ""; | |
| for (let i = 0; i < u.length; i += 0x8000) s += String.fromCharCode(...u.subarray(i, i + 0x8000)); | |
| return btoa(s); | |
| } | |
| function bufFromB64(s) { return Uint8Array.from(atob(s), c => c.charCodeAt(0)).buffer; } | |
| function makeRelayChannel(peerId) { | |
| return { | |
| isRelay: true, | |
| get readyState() { return ws && ws.readyState === 1 && names.has(peerId) ? "open" : "closed"; }, | |
| get bufferedAmount() { return ws ? ws.bufferedAmount : 0; }, | |
| send(buf) { ws.send(JSON.stringify({ type: "relay", to: peerId, data: b64FromBuf(buf) })); }, | |
| close() { chans.delete(peerId); }, | |
| }; | |
| } | |
| const reconnectTimers = new Map(); | |
| function scheduleReconnect(peerId, attempt = 0) { | |
| if (!names.has(peerId) || chans.has(peerId) || reconnectTimers.has(peerId)) return; | |
| if (attempt >= MAX_DIRECT_ATTEMPTS) return void useRelay(peerId); | |
| const delay = 1500 * Math.pow(2, attempt); | |
| reconnectTimers.set(peerId, setTimeout(() => { | |
| reconnectTimers.delete(peerId); | |
| if (!names.has(peerId) || chans.has(peerId)) return; | |
| if (+myId.slice(1) > +peerId.slice(1)) { | |
| log(`reconnecting to ${nmeOf(peerId)} (attempt ${attempt + 1})…`); | |
| cleanupPeer(peerId); initiatePeer(peerId); | |
| } else { | |
| armWatchdog(peerId); // the other side dials; still deadline it | |
| } | |
| setTimeout(() => scheduleReconnect(peerId, attempt + 1), CONNECT_TIMEOUT + 2000); | |
| }, delay)); | |
| } | |
| function initiatePeer(peerId) { | |
| if (forceRelay) return useRelay(peerId); // ?relay=1 — skip WebRTC entirely | |
| const pc = newPC(peerId); | |
| setupChannel(peerId, pc.createDataChannel("daisy")); | |
| armWatchdog(peerId); // no silent forever-connecting | |
| pc.createOffer().then(o => pc.setLocalDescription(o)).then(() => signal(peerId, { sdp: pc.localDescription })); | |
| } | |
| const pendingCand = new Map(); | |
| async function onSignal(from, data) { | |
| let pc = pcs.get(from); | |
| if (data.sdp) { | |
| if (!pc) { | |
| pc = newPC(from); | |
| pc.ondatachannel = (e) => setupChannel(from, e.channel); | |
| armWatchdog(from); // the answering side needs a deadline too | |
| } | |
| await pc.setRemoteDescription(data.sdp); | |
| for (const c of pendingCand.get(from) || []) try { await pc.addIceCandidate(c); } catch (e) {} | |
| pendingCand.delete(from); | |
| if (data.sdp.type === "offer") { | |
| const ans = await pc.createAnswer(); await pc.setLocalDescription(ans); | |
| signal(from, { sdp: pc.localDescription }); | |
| } | |
| } else if (data.candidate) { | |
| if (pc && pc.remoteDescription) { try { await pc.addIceCandidate(data.candidate); } catch (e) {} } | |
| else { if (!pendingCand.has(from)) pendingCand.set(from, []); pendingCand.get(from).push(data.candidate); } | |
| } | |
| } | |
| function setupChannel(peerId, dc) { | |
| dc.binaryType = "arraybuffer"; | |
| dc.onopen = () => { | |
| clearWatchdog(peerId); | |
| attempts.delete(peerId); | |
| relayed.delete(peerId); | |
| chans.set(peerId, dc); updatePeers(); | |
| log(`connected to ${nmeOf(peerId)}`); | |
| const pc = pcs.get(peerId); | |
| if (pc) logSelectedPath(peerId, pc); | |
| sendHello(peerId); | |
| }; | |
| dc.onclose = () => { chans.delete(peerId); updatePeers(); wake(); scheduleReconnect(peerId); }; | |
| dc.onmessage = (e) => onWire(peerId, e.data); | |
| } | |
| function cleanupPeer(id) { | |
| clearWatchdog(id); | |
| const pc = pcs.get(id); if (pc) pc.close(); | |
| pcs.delete(id); pendingCand.delete(id); | |
| const dc = chans.get(id); | |
| if (dc && !dc.isRelay) chans.delete(id); // keep a working relay channel | |
| for (const k of fragIn.keys()) if (k.startsWith(id + ":")) fragIn.delete(k); | |
| wake(); | |
| } | |
| // ---- fragmentation ---------------------------------------------------------- | |
| const FRAG_SENTINEL = Wire.FRAG, FRAG_CHUNK = 48 * 1024, DC_MAXBUF = 4 * 1024 * 1024; | |
| let fragSeq = 1; | |
| const fragIn = new Map(); | |
| function dcDrain(dc) { | |
| return new Promise((res) => { | |
| if (dc.bufferedAmount <= DC_MAXBUF || dc.readyState !== "open") return res(); | |
| const t = setInterval(() => { | |
| if (dc.bufferedAmount <= DC_MAXBUF || dc.readyState !== "open") { clearInterval(t); res(); } | |
| }, 50); | |
| }); | |
| } | |
| async function dcSend(dc, buf) { | |
| if (buf.byteLength <= FRAG_CHUNK) { | |
| await dcDrain(dc); | |
| if (dc.readyState === "open") dc.send(buf); | |
| return; | |
| } | |
| const id = fragSeq++, src = new Uint8Array(buf); | |
| const total = Math.ceil(src.length / FRAG_CHUNK); | |
| for (let s = 0; s < total; s++) { | |
| await dcDrain(dc); | |
| if (dc.readyState !== "open") return; | |
| const part = src.subarray(s * FRAG_CHUNK, Math.min((s + 1) * FRAG_CHUNK, src.length)); | |
| const msg = new ArrayBuffer(16 + part.length); | |
| new Int32Array(msg, 0, 4).set([FRAG_SENTINEL, id, s, total]); | |
| new Uint8Array(msg, 16).set(part); | |
| dc.send(msg); | |
| } | |
| } | |
| function onFragment(peerId, buf) { | |
| const [, id, seq, total] = new Int32Array(buf, 0, 4); | |
| const key = peerId + ":" + id; | |
| let st = fragIn.get(key); | |
| if (!st) { st = { id, parts: [], got: 0, total }; fragIn.set(key, st); } | |
| st.parts[seq] = new Uint8Array(buf, 16).slice(0); | |
| st.got++; | |
| if (st.got < st.total) return null; | |
| fragIn.delete(key); | |
| let len = 0; for (const p of st.parts) len += p.length; | |
| const out = new Uint8Array(len); | |
| let off = 0; for (const p of st.parts) { out.set(p, off); off += p.length; } | |
| return out.buffer; | |
| } | |
| function sendTo(peerId, buf) { | |
| const dc = chans.get(peerId); | |
| if (!dc || dc.readyState !== "open") return Promise.resolve(false); | |
| return dcSend(dc, buf).then(() => true); | |
| } | |
| function broadcast(buf) { | |
| return Promise.all([...chans.values()].filter(dc => dc.readyState === "open").map(dc => dcSend(dc, buf))); | |
| } | |
| // ---- hello / capability ----------------------------------------------------- | |
| function sendHello(peerId) { | |
| return sendTo(peerId, Wire.packHello(myCapacity, probeHash, compute ? compute.backend : "?")); | |
| } | |
| function onHello(peerId, buf) { | |
| let h; | |
| try { h = Wire.unpackHello(buf); } catch (e) { log(`bad hello from ${nmeOf(peerId)}: ${e.message}`); return; } | |
| caps.set(peerId, { capacity: h.capacity, backend: h.backend, probe: h.probeHash }); | |
| // The kernel probe matters MORE in a pipeline than in the trainer. There, | |
| // every peer computes the same thing, so bad arithmetic shows up as a | |
| // diverging replica. Here each stage computes something different and nobody | |
| // repeats it — a broken middle stage would corrupt every token invisibly. | |
| // The probe is the one value that stays comparable when the work is not. | |
| if (probeHash && h.probeHash && h.probeHash !== probeHash) | |
| log(`⚠ ${nmeOf(peerId)} disagrees with this device's kernel probe (${h.probeHash} vs ${probeHash}). ` + | |
| `Their arithmetic differs from ours — do not put them in the ring.`); | |
| renderPlan(); | |
| } | |
| // ---- capacity --------------------------------------------------------------- | |
| let myCapacity = 1; | |
| async function measureCapacity() { | |
| const m = 32, k = 64, n = 64; | |
| const X = new Float32Array(m * k), W = new Float32Array(k * n); | |
| for (let i = 0; i < X.length; i++) X[i] = Math.sin(i) * 0.5; | |
| for (let i = 0; i < W.length; i++) W[i] = Math.cos(i) * 0.5; | |
| await Verified.vgemmBlock(X, W, { m, k, n, batch: 1 }, L, compute.bgemm, null); | |
| const t0 = performance.now(); | |
| let it = 0; | |
| while (performance.now() - t0 < 300) { await Verified.vgemmBlock(X, W, { m, k, n, batch: 1 }, L, compute.bgemm, null); it++; } | |
| myCapacity = it / ((performance.now() - t0) / 1000); | |
| return myCapacity; | |
| } | |
| // ---- loading a model from the Hub ------------------------------------------- | |
| // Only the header and config are read here: a few tens of KB, regardless of how | |
| // large the model is. Weights are fetched per stage, later, by each device. | |
| async function loadRepo() { | |
| const repo = ui.repo.value.trim(); | |
| const revision = (ui.revision.value.trim() || "main"); | |
| if (!/^[\w.-]+\/[\w.-]+$/.test(repo)) { log(`"${repo}" is not a valid repo id (expected owner/name)`); return; } | |
| ui.loadRepo.disabled = true; | |
| ui.modelState.textContent = "reading config…"; | |
| try { | |
| const cfg = await withAuth((t) => HF.getJSON(repo, "config.json", revision, t), `${repo}/config.json`); | |
| const s = Arch.fromConfig(cfg); | |
| ui.modelState.textContent = "reading tokenizer…"; | |
| const tokJson = await withAuth((t) => HF.getJSON(repo, "tokenizer.json", revision, t), `${repo}/tokenizer.json`); | |
| tok = Tokenizer.build(tokJson); | |
| ui.tokenizer.textContent = tok.name; | |
| ui.modelState.textContent = "reading weight index…"; | |
| const { tensors } = await withAuth((t) => HF.readIndex(repo, revision, t, (m) => { ui.modelState.textContent = m; }), | |
| `${repo} weights`); | |
| const fingerprint = Shard.modelFingerprint(repo, revision, tensors); | |
| repoInfo = { repo, revision, spec: s, tensors, fingerprint }; | |
| spec = s; modelHash = fingerprint; | |
| const totalMB = [...tensors.values()].reduce((a, t) => a + t.elems * 4, 0) / 1048576; | |
| ui.model.textContent = | |
| `${repo} · ${s.family}-style · ${s.layers} layers · hidden ${s.hidden} · ` + | |
| `heads ${s.heads}${s.kvHeads !== s.heads ? "/" + s.kvHeads + " kv" : ""} · vocab ${s.vocab}`; | |
| ui.modelState.textContent = `${totalMB.toFixed(0)} MB in f32 · fingerprint ${fingerprint.toString(16)}`; | |
| log(`model ready: ${repo}@${revision} — ${s.layers} layers, ${totalMB.toFixed(0)} MB total in f32. ` + | |
| `Nothing has been downloaded yet beyond headers; each device will fetch only its own layers.`); | |
| if (s.maxPos < ctxLen()) log(`⚠ this model's max position is ${s.maxPos} — lower the context length`); | |
| ui.genBtn.disabled = false; | |
| ui.verifyRow.style.display = ""; | |
| renderPlan(); | |
| } catch (e) { | |
| ui.modelState.textContent = "failed"; | |
| log(`could not load ${repo}: ${redact(e.message)}`); | |
| } | |
| ui.loadRepo.disabled = false; | |
| } | |
| function buildPlan() { | |
| if (!repoInfo) return null; | |
| const list = [{ id: myId, capacity: myCapacity, backend: compute.backend }]; | |
| for (const id of [...chans.keys()].sort((a, b) => +a.slice(1) - +b.slice(1))) { | |
| const c = caps.get(id); | |
| if (c) list.push({ id, capacity: c.capacity, backend: c.backend }); | |
| } | |
| return Shard.planStages(repoInfo.spec, list); | |
| } | |
| function renderPlan() { | |
| if (!plan) { | |
| const known = 1 + [...chans.keys()].filter(id => caps.has(id)).length; | |
| ui.plan.textContent = repoInfo ? `${known} device(s) ready — press Generate to shard and run` | |
| : "load a model to see how it would be split"; | |
| return; | |
| } | |
| ui.plan.textContent = plan.map(s => { | |
| const who = s.id === myId ? "me" : nmeOf(s.id); | |
| const part = s.hi > s.lo ? `layers ${s.lo}–${s.hi - 1}` : "—"; | |
| let mb = ""; | |
| if (repoInfo) mb = " · " + (Shard.stageBytes(repoInfo.spec, repoInfo.tensors, s, Arch) / 1048576).toFixed(0) + " MB"; | |
| return `${s.index}. ${who}${s.head ? " (head: embed + lm_head)" : ""} · ${part}${mb}` + | |
| (readyStages.has(s.index) ? " ✓" : ""); | |
| }).join("\n"); | |
| } | |
| // Fetch this device's own layers. This is the only place weight bytes move, | |
| // and they move from the Hub to here — never from a peer. | |
| async function loadMyStage(assign, st) { | |
| const s = assign.spec; | |
| ui.myShard.textContent = "reading index…"; | |
| const { tensors } = await withAuth((t) => HF.readIndex(assign.repo, assign.revision, t), `${assign.repo} weights`); | |
| const want = Arch.tensorsFor(s, tensors, st).map(n => tensors.get(n)); | |
| const mb = want.reduce((a, t) => a + t.elems * 4, 0) / 1048576; | |
| log(`fetching my slice from the Hub: ${want.length} tensors, ${mb.toFixed(0)} MB ` + | |
| `(the other ${s.layers - (st.hi - st.lo)} layers are never downloaded here)`); | |
| const got = await withAuth((t) => HF.fetchTensors(assign.repo, assign.revision, t, want, | |
| (p) => { ui.myShard.textContent = `downloading ${p}`; }), | |
| `${assign.repo} weights`); | |
| const w = Shard.stageWeights(s, st, got, Arch); | |
| stageObj = Infer.makeStage(s, st, w, kernelCtx(), assign.ctx || ctxLen()); | |
| spec = s; | |
| ui.myShard.textContent = `${st.head ? "head + " : ""}layers ${st.lo}–${st.hi - 1} · ${mb.toFixed(0)} MB resident`; | |
| return stageObj; | |
| } | |
| async function onAssign(peerId, buf) { | |
| if (running) { log(`ignored an assignment from ${nmeOf(peerId)} mid-run`); return; } | |
| let a; | |
| try { a = Wire.unpackAssign(buf); } catch (e) { log(`REFUSED assignment from ${nmeOf(peerId)}: ${e.message}`); return; } | |
| plan = a.plan; spec = a.spec; modelHash = a.fingerprint; | |
| myStage = plan[a.mine]; | |
| log(`assigned by ${nmeOf(peerId)}: ${a.repo}@${a.revision}, layers ${myStage.lo}–${myStage.hi - 1} ` + | |
| `of ${a.spec.layers} (stage ${a.mine} of ${plan.length})`); | |
| renderPlan(); | |
| try { | |
| // tokenizer only matters on the head, but loading it is cheap and lets a | |
| // middle stage show the token stream | |
| if (!tok) { | |
| try { tok = Tokenizer.build(await withAuth((t) => HF.getJSON(a.repo, "tokenizer.json", a.revision, t), "tokenizer")); } | |
| catch (e) { log(`(no tokenizer here: ${redact(e.message)} — this stage will show ids, not text)`); } | |
| } | |
| await loadMyStage(a, myStage); | |
| await sendTo(peerId, Wire.packReady(a.mine, true, "loaded")); | |
| log(`ready — holding layers ${myStage.lo}–${myStage.hi - 1}. The rest of the model is not on this device.`); | |
| setStatus("in the ring — waiting for work"); | |
| } catch (e) { | |
| await sendTo(peerId, Wire.packReady(a.mine, false, redact(e.message))); | |
| log(`could not take my slice: ${redact(e.message)}`); | |
| } | |
| } | |
| function onReady(peerId, buf) { | |
| const r = Wire.unpackReady(buf); | |
| if (r.ok) { readyStages.add(r.stageIndex); log(`${nmeOf(peerId)} is ready (stage ${r.stageIndex})`); } | |
| else log(`${nmeOf(peerId)} could not load stage ${r.stageIndex}: ${r.note}`); | |
| renderPlan(); wake(); | |
| } | |
| // ---- the ring --------------------------------------------------------------- | |
| // One token = one lap. The head embeds the window and hands the hidden state | |
| // to stage 1; each stage runs its own layers and hands the result on; the last | |
| // stage returns it to the head, which turns it into a token. | |
| // | |
| // The payload is T x hidden floats — tens of KB. The weights it stands in for | |
| // are hundreds of megabytes. That asymmetry is the whole reason this works, | |
| // and it makes a run latency-bound rather than bandwidth-bound. | |
| let runSeq = 0; | |
| const waiters = new Set(); | |
| function wake() { for (const w of waiters) w(); } | |
| function waitFor(pred, timeoutMs) { | |
| return new Promise((resolve) => { | |
| const t0 = Date.now(); | |
| let timer = null; | |
| const check = () => { | |
| const ok = pred(); | |
| if (ok || Date.now() - t0 > timeoutMs) { waiters.delete(check); clearInterval(timer); resolve(!!ok); } | |
| }; | |
| waiters.add(check); | |
| timer = setInterval(check, 200); | |
| check(); | |
| }); | |
| } | |
| const actIn = new Map(); | |
| function packAct(seq, tokenIdx, nextIndex, hidden) { | |
| return Wire.packAct(seq, tokenIdx, nextIndex, hidden, Shard.hashF32, modelHash); | |
| } | |
| async function onAct(peerId, buf) { | |
| let a; | |
| try { a = Wire.unpackAct(buf, Shard.hashF32); } | |
| catch (e) { log(`activation from ${nmeOf(peerId)} rejected: ${e.message}`); return; } | |
| const { seq, tokenIdx, nextIndex, modelHash: mh, hidden } = a; | |
| if (mh !== modelHash) { | |
| log(`REFUSED activation from ${nmeOf(peerId)}: it belongs to model ${mh.toString(16)}, this device ` + | |
| `holds a slice of ${modelHash.toString(16)}. Mixing two models would produce confident nonsense.`); | |
| return; | |
| } | |
| const me = plan && plan.find(s => s.id === myId); | |
| const kind = Wire.classifyAct(nextIndex, me ? me.index : -99); | |
| if (kind === "return") { actIn.set(`${seq}:${tokenIdx}`, hidden); wake(); return; } | |
| if (kind === "other" || !me || !stageObj) return; | |
| const t0 = performance.now(); | |
| let x; | |
| try { x = await Infer.runLayers(stageObj, hidden); } | |
| catch (e) { log(`stage failed: ${redact(e.message)}`); return; } | |
| if (auditFailure) { log("KERNEL AUDIT FAILED mid-hop — refusing to pass on a value I cannot vouch for"); return; } | |
| const hop = Wire.routeAfter(plan, me.index); | |
| ui.ringState.textContent = `layers ${me.lo}–${me.hi - 1} in ${(performance.now() - t0).toFixed(0)} ms → ` + | |
| `${hop.to === myId ? "me" : nmeOf(hop.to)}${hop.isReturn ? " (return)" : ""}`; | |
| await sendTo(hop.to, packAct(seq, tokenIdx, hop.address, x)); | |
| } | |
| async function generate() { | |
| if (running) return; | |
| if (!repoInfo) { log("load a model first — the device that loads it drives the ring"); return; } | |
| if (!tok) { log("no tokenizer loaded"); return; } | |
| running = true; abortRun = false; auditFailure = null; | |
| ui.genBtn.disabled = true; ui.stopBtn.disabled = false; | |
| readyStages.clear(); | |
| try { | |
| const T = Math.min(ctxLen(), repoInfo.spec.maxPos); | |
| plan = buildPlan(); modelHash = repoInfo.fingerprint; spec = repoInfo.spec; | |
| renderPlan(); | |
| if (plan.length > 1) { | |
| log(`plan: ${spec.layers} layers over ${plan.length} device(s) — ` + | |
| plan.map(s => `${s.id === myId ? "me" : nmeOf(s.id)}:${s.hi - s.lo}`).join(", ")); | |
| for (let i = 1; i < plan.length; i++) | |
| await sendTo(plan[i].id, Wire.packAssign({ | |
| repo: repoInfo.repo, revision: repoInfo.revision, spec, plan, | |
| mine: i, fingerprint: repoInfo.fingerprint, ctx: T, | |
| })); | |
| log("waiting for every stage to fetch its layers…"); | |
| const ok = await waitFor(() => readyStages.size >= plan.length - 1, 600000); | |
| if (!ok) { log("not every stage reported ready — generating anyway will stall, so stopping here."); throw new Error("stages not ready"); } | |
| } else { | |
| log(`solo run — this device will hold all ${spec.layers} layers`); | |
| } | |
| myStage = plan[0]; | |
| ui.myShard.textContent = "loading my slice…"; | |
| await loadMyStage({ repo: repoInfo.repo, revision: repoInfo.revision, spec, ctx: T }, myStage); | |
| const seq = ++runSeq; | |
| const nTok = +ui.ntok.value, temp = +ui.temp.value / 100; | |
| const opts = { temperature: temp, topK: 40, rng: Infer.mulberry32(+ui.seed.value | 0) }; | |
| let ids = [...Tokenizer.encode(tok, ui.prompt.value || "The")]; | |
| if (!ids.length) ids = [0]; | |
| const promptLen = ids.length; | |
| ui.out.textContent = ui.prompt.value; | |
| const tStart = performance.now(); | |
| let hops = 0; | |
| for (let n = 0; n < nTok && !abortRun; n++) { | |
| const win = new Int32Array(T); | |
| const tail = ids.slice(-T); | |
| for (let i = 0; i < tail.length; i++) win[T - tail.length + i] = tail[i]; | |
| let x = Infer.embed(stageObj, win); | |
| x = await Infer.runLayers(stageObj, x); | |
| if (auditFailure) { log(`KERNEL AUDIT FAILED: ${auditFailure} — stopping`); break; } | |
| if (plan.length > 1) { | |
| await sendTo(plan[1].id, packAct(seq, n, plan[1].index, x)); | |
| hops += plan.length; | |
| const key = `${seq}:${n}`; | |
| const ok = await waitFor(() => actIn.has(key), 60000); | |
| if (!ok) { log(`the ring stalled at token ${n + 1}: no activation came back. Press Generate again to re-plan.`); break; } | |
| x = actIn.get(key); actIn.delete(key); | |
| } | |
| const id = Infer.pickToken(await Infer.readout(stageObj, x), opts); | |
| ids.push(id); | |
| ui.out.textContent = Tokenizer.decode(tok, ids); | |
| ui.tokcount.textContent = String(n + 1); | |
| ui.hops.textContent = String(hops); | |
| ui.tps.textContent = ((n + 1) / ((performance.now() - tStart) / 1000)).toFixed(2); | |
| broadcast(Wire.packToken(seq, id, ids.length)); | |
| await new Promise(r => setTimeout(r, 0)); | |
| } | |
| // The differential check. In a pipeline nothing recomputes anything, so | |
| // this is the only instrument that can show a distributed answer is RIGHT | |
| // rather than merely self-consistent. It costs a full local run, which is | |
| // only possible when this device can hold the whole model — so it is | |
| // offered, not assumed. | |
| if (ui.verify.checked && !abortRun && plan.length > 1) { | |
| log("verifying: fetching the whole model here and re-running the same prompt…"); | |
| const all = { id: myId, index: 0, lo: 0, hi: spec.layers, head: true }; | |
| const solo = await loadMyStage({ repo: repoInfo.repo, revision: repoInfo.revision, spec, ctx: T }, all); | |
| const ref = await Infer.generateLocal([solo], Tokenizer.encode(tok, ui.prompt.value || "The"), nTok, | |
| { temperature: temp, topK: 40, rng: Infer.mulberry32(+ui.seed.value | 0) }); | |
| const same = ref.length === ids.length && ref.every((v, i) => v === ids[i]); | |
| log(same ? "VERIFIED: the distributed run and the single-device run produced identical token ids." | |
| : `MISMATCH: the ring and this device disagree. Distributed "${Tokenizer.decode(tok, ids).slice(0, 60)}" ` + | |
| `vs local "${Tokenizer.decode(tok, ref).slice(0, 60)}". Check the log for probe warnings.`); | |
| } | |
| if (!abortRun) log(`done — ${ids.length - promptLen} token(s) in ${((performance.now() - tStart) / 1000).toFixed(1)} s ` + | |
| `over ${plan.length} stage(s)`); | |
| broadcast(Wire.packDone(seq)); | |
| } catch (e) { | |
| log(`run failed: ${redact(e.message)}`); | |
| console.error(e); | |
| } | |
| running = false; | |
| ui.genBtn.disabled = false; ui.stopBtn.disabled = true; | |
| ui.ringState.textContent = "idle"; | |
| } | |
| function onToken(peerId, buf) { | |
| const { id } = Wire.unpackToken(buf); | |
| if (tok) ui.out.textContent += Tokenizer.decode(tok, [id]); | |
| else ui.out.textContent += ` ${id}`; | |
| ui.tokcount.textContent = String(+(ui.tokcount.textContent || 0) + 1); | |
| } | |
| function onWire(peerId, buf) { | |
| const tag = Wire.tagOf(buf); | |
| if (tag === Wire.FRAG) { const whole = onFragment(peerId, buf); if (whole) onWire(peerId, whole); return; } | |
| if (tag === Wire.HELLO) return onHello(peerId, buf); | |
| if (tag === Wire.ASSIGN) return void onAssign(peerId, buf); | |
| if (tag === Wire.READY) return onReady(peerId, buf); | |
| if (tag === Wire.ACT) return void onAct(peerId, buf); | |
| if (tag === Wire.TOKEN) return onToken(peerId, buf); | |
| if (tag === Wire.DONE) { ui.ringState.textContent = "idle"; return; } | |
| } | |
| // ---- kernels ---------------------------------------------------------------- | |
| const gemmAudit = { | |
| cells: 12, | |
| due: () => true, | |
| fail: (msg) => { if (!auditFailure) { auditFailure = msg; log(`KERNEL AUDIT FAILED: ${msg}`); } }, | |
| }; | |
| function kernelCtx() { | |
| return { L, bgemm: compute.bgemm || null, att: compute.att || null, | |
| mlp: compute.mlp || null, audit: gemmAudit }; | |
| } | |
| // ---- boot ------------------------------------------------------------------- | |
| (async function () { | |
| try { | |
| const mode = await (await fetch("mode")).json(); | |
| if (mode.rtc) rtcConfig = mode.rtc; | |
| oauthAvailable = !!mode.oauth; | |
| // Room-first deployments (the hosted Space sets DAISY_FORCE_ROOMS): there | |
| // is no LAN auto-grouping between strangers. Two people behind the same | |
| // CGNAT or on the same campus network share a public IP, and without this | |
| // they would be dropped into one ring together — able to see each other's | |
| // addresses and each other's activations. So a visitor with no room code | |
| // chooses: create their own room, or join one by code. | |
| if (mode.forceRooms && !room()) { | |
| for (const c of document.querySelectorAll(".card")) if (c !== ui.lobby) c.style.display = "none"; | |
| ui.lobby.style.display = ""; | |
| ui.createRoom.onclick = () => { | |
| const code = (ADJ[Math.floor(Math.random() * ADJ.length)] + "-" + | |
| NOUN[Math.floor(Math.random() * NOUN.length)] + "-" + | |
| (100 + Math.floor(Math.random() * 900))).toLowerCase(); | |
| location.href = "?room=" + code; | |
| }; | |
| const join = () => { | |
| const code = ui.joinCode.value.trim().toLowerCase(); | |
| if (code) location.href = "?room=" + encodeURIComponent(code); | |
| }; | |
| ui.joinRoom.onclick = join; | |
| ui.joinCode.onkeydown = (e) => { if (e.key === "Enter") join(); }; | |
| return; // wait for the choice | |
| } | |
| } catch (e) {} // no /mode: local defaults | |
| adoptTokenFromFragment(); // returning from an OAuth sign-in | |
| // Same rule as the trainer: the verified units are mandatory. No float path. | |
| try { | |
| L = await Compute.loadLUTs(); | |
| if (!(L.mul instanceof Int16Array) || L.mul.length !== 65536) throw new Error("mul8 LUT malformed"); | |
| if (L.mul[((7 & 0xFF) * 256) + (-3 & 0xFF)] !== -21) throw new Error("mul8 LUT self-test failed (7 × -3 ≠ -21)"); | |
| compute = await Compute.initCompute(L); | |
| } catch (e) { | |
| setStatus("NEURAL UNITS UNAVAILABLE — inference disabled"); | |
| ui.backend.textContent = "unavailable"; | |
| log(`FATAL: verified neural units failed to load (${e.message}). This build only computes through the units.`); | |
| return; | |
| } | |
| ui.backend.textContent = `${compute.backend.toUpperCase()} — ${compute.label}`; | |
| probeHash = await Compute.kernelProbe(compute, L); | |
| log(`kernel probe: ${probeHash} — every honest device gets this same number, on any backend`); | |
| await measureCapacity(); | |
| log(`measured capacity: ${myCapacity.toFixed(0)} verified GEMMs/sec — this decides how many layers this device takes`); | |
| ui.me.textContent = deviceName; | |
| if (!hfToken) setToken(null); // keep a token adopted from the fragment | |
| if (oauthAvailable) log("this deployment uses Hugging Face sign-in for gated models — no token is ever typed into this page"); | |
| for (const [el, out] of [[ui.temp, ui.vtemp], [ui.ntok, ui.vntok], [ui.ctxLen, ui.vctxLen]]) | |
| el.oninput = () => out.textContent = el.value; | |
| ui.temp.oninput(); ui.ntok.oninput(); ui.ctxLen.oninput(); | |
| if (room()) { | |
| ui.roomInfo.style.display = ""; | |
| ui.roomCode.textContent = room(); | |
| ui.copyLink.onclick = async () => { | |
| try { await navigator.clipboard.writeText(location.href); ui.copyLink.textContent = "Copied!"; } | |
| catch { ui.copyLink.textContent = location.href; } | |
| setTimeout(() => ui.copyLink.textContent = "Copy invite link", 2000); | |
| }; | |
| } | |
| ui.loadRepo.onclick = loadRepo; | |
| ui.repo.onkeydown = (e) => { if (e.key === "Enter") loadRepo(); }; | |
| ui.clearToken.onclick = clearToken; | |
| ui.genBtn.onclick = generate; | |
| ui.stopBtn.onclick = () => { abortRun = true; log("stopping after the current token"); }; | |
| updatePeers(); renderPlan(); | |
| connectSignaling(); | |
| document.addEventListener("visibilitychange", () => { | |
| if (!document.hidden && (!ws || ws.readyState > 1)) { log("page woke — reconnecting"); connectSignaling(); } | |
| }); | |
| if (navigator.connection && navigator.connection.addEventListener) | |
| navigator.connection.addEventListener("change", () => { | |
| if (!ws || ws.readyState > 1) { log("network changed — reconnecting"); connectSignaling(); } | |
| }); | |
| setStatus("ready — load a model to drive, or wait to be given a slice"); | |
| log(`ready — this device is "${deviceName}", computing through verified units on ${compute.backend.toUpperCase()}`); | |
| })(); | |