1// Pre-flight: can this browser hold layers, and if not, what exactly to do about it. The verdict 2// is a pure function of what the browser reports, so it can be unit tested; probe() gathers it. 3 4// iPadOS reports a Mac user agent; a touch screen gives it away 5export function deviceKind({ ua = "", touchPoints = 0, mobile = false } = {}) { 6 if (/iPhone|iPod/.test(ua)) return "iPhone"; 7 if (/iPad/.test(ua) || (/Macintosh/.test(ua) && touchPoints > 1)) return "iPad"; 8 if (/Android/.test(ua)) return mobile || /Mobile/.test(ua) ? "Android" : "Android tablet"; 9 if (/Mac/.test(ua)) return "Mac"; 10 return "Device"; 11} 12 13// facts: { ua, secure, hasGpuApi, adapter: null | { vendor, architecture }, touchPoints, mobile } 14// -> { ok, kind, line, detail }: one calm sentence (what this device can do), and the remedy 15// (what to change for WebGPU) for a Details disclosure. Phones are checked first, in phone words. 16const PHONE = "This phone's GPU isn't available to the browser. It can still join and chat."; 17const DESK = "This browser can't use its graphics chip, so this device can chat but not help run the model. Chrome or Edge on a laptop works best."; 18export function verdict(f) { 19 const kind = deviceKind(f); 20 const ua = f.ua || ""; 21 if (f.adapter) { 22 const g = [f.adapter.vendor, f.adapter.architecture].filter(
22Boolean).join(" "); 23 return { ok: true, kind, line: `WebGPU works here${g ? ` (${g})` : ""}: this ${kind === "Device" ? "device" : kind} can hold part of the model.`, detail: "" }; 24 } 25 const no = (line, detail) => ({ ok: false, kind, line, detail }); 26 const phone = kind === "iPhone" || kind === "iPad" || kind === "Android" || kind === "Android tablet"; 27 if (phone) { 28 if (!f.secure) return no(PHONE, "WebGPU needs a secure page: open this over https."); 29 if (kind === "iPhone" || kind === "iPad") return no(PHONE, `WebGPU on an ${kind} needs Safari 26 (iOS 26) or later: update iOS.`); 30 if (f.hasGpuApi) return no(PHONE, "The browser has WebGPU but offered no GPU on this phone (a blocklisted driver?). Chrome 121 or later may help."); 31 return no(PHONE, "This Android browser has no WebGPU: use Chrome 121 or later."); 32 } 33 // when the fix is in this browser (a flag, an update), the line says so instead of "use Chrome", 34 // and the page shows the remedy open, not behind a collapsed Details 35 const CHAT = "so this device can chat but not help run the model yet."; 36 if (!f.secure) return no(`This page isn't secure, ${CHAT}`, "WebGPU needs a secure page: open this over https (or localhost)."); 37 if (f.hasGpuApi) return no(`This browser has WebGPU but found no usable GPU, ${CHAT}`, "A blocklisted driver? Try chrome://flags/#enable-unsafe-webgpu, then reload, or another browser."); 38 if (/Firefox\//.test(ua)) return no(`Firefox has no WebGPU here, ${CHAT}`, "Use Chrome or Edge 113+, or Safari 26+."); 39 if (/Linux/.test(ua) && /Chrome\//.test(ua) && !/Android/.test(ua)) 40 return no(`WebGPU is off in Chrome on Linux, ${CHAT} Two flags turn it on:`, "Enable chrome://flags/#enable-unsafe-webgpu and chrome://flags/#enable-vulkan, then reload."); 41 if (/Safari\//.test(ua) && !/Chrome\//.test(ua)) return no(`This Safari has no WebGPU, ${CHAT}`, "Safari 26 has it: update macOS, or use Chrome."); 42 return no(DESK, "Use Chrome or Edge 113+, or Safari 26+."); 43} 44 45export async function probe() { 46 const f = { 47 ua: navigator.userAgent, secure: window.isSecureContext, hasGpuApi: !!navigator.gpu, 48 touchPoints: navigator.maxTouchPoints || 0, mobile: !!navigator.userAgentData?.mobile, adapter: null, 49 }; 50 if (navigator.gpu) { 51 try { 52 const a = await navigator.gpu.requestAdapter(); 53 if (a) f.adapter = { vendor: a.info?.vendor || "", architecture: a.info?.architecture || "" }; 54 } catch {} 55 } 56 return verdict(f); 57}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.