Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions LOADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -1485,6 +1485,81 @@ WHERE event IN ('stockfish_engine_ready', 'stockfish_engine_failed') AND timesta
GROUP BY after_round_10
```

### Round 13: getting past the Maia download

PostHog showed the biggest loss on the site isn't a slow page. Of the 7,631 people who
saw the full-page Maia download prompt in 30 days, 37.6% never started the download
(funnel to 2026-11-05), about 780 people in the last full week, or 11% of that week's
visitors. Of those who didn't start, 65% did nothing else within a minute, against 25% of
those who did. Both groups differ in other ways, so that doesn't prove the prompt caused it.

Stockfish doesn't wait for Maia, so nothing about the page needs the prompt to block it.

| what changed | before | after |
| ------------------------ | ----------------------------------- | --------------------------------------------------------------------------- |
| desktop | full-page prompt, needs a click | page opens, Maia downloads by itself, small card shows progress |
| phone (or save-data, 2g) | full-page prompt, only exit is home | same size prompt, shorter text, "Not now" opens the page |
| after "Not now" | - | button in the empty Maia panel, and the prompt again in a new tab |
| puzzles | prompt on arrival | nothing until the puzzle is over |
| drills | prompt on arrival | choosing a drill isn't blocked; Maia is needed to start one |
| progress | stuck on "Starting download..." | size and time left: "12 of 42MB, about 20 s left" |
| failed download | logged as completed | logged as failed, with "Try again" |
| saved model | browser free to delete it | browser asked to keep it (not Firefox, which would show a popup) |
| saving it | "saved" before the save was final | waits for the save; a failure is reported, and Maia still runs on that page |

The progress bar never moved because the model is sent with brotli, so the browser gets no
`Content-Length`. The page now knows the size. Progress reaches the page's state in 10%
steps as before, and the bar reads finer steps directly, so the page doesn't re-render more.

"42 MB" is what the download uses: 41,688,610 bytes as sent, 45,683,686 once decompressed.

**Half of new visitors keep the prompt.** `maia_prompt_variant` is `wall` or `no_wall`,
chosen once per browser only when it has no model, and sent with every event, so the groups
can be compared for two weeks. On phones the comparison is "Return Home" against "Not now".

**Stockfish** (test browser, model at about the median 9 s, paired medians, later minus
earlier; phone-sized is not slowed down):

| comparison | visits | d12 | d18 |
| -------------------------------------------- | ------- | ------ | ------ |
| return visit, desktop: this against main | 8 pairs | +46 ms | -5 ms |
| return visit, phone-sized: this against main | 8 pairs | -56 ms | -38 ms |
| first visit, desktop: downloading or not | 6 pairs | -3 ms | +30 ms |

All are within the spread between visits. In an earlier run of the first-visit comparison,
2 of 6 visits reached d18 up to 0.5 s later while downloading. On a first desktop visit
Maia's moves appeared about 11.8 s after the board, with no click.

**Asked again after finishing the download** (60 days, per person, operating system and
browser; clearing cookies looks like a new person, so these are low):

| OS | asked again at all | within an hour | of those back a day or more later |
| ------- | ------------------ | -------------- | --------------------------------- |
| iOS | 7.9% | 5.7% | 4.7% |
| Android | 7.0% | 3.8% | 12.0% |
| Mac | 4.5% | 2.5% | 6.7% |
| Windows | 2.4% | 1.3% | 3.5% |

New events say why from now on: `maia_model_lost` (with `days_since_saved`),
`maia_model_store_failed`, `maia_model_protected` (with `granted`). Also new:
`download_model_dismissed`, and `trigger` on `download_model_initiated` (`wall`, `auto`,
`tap`, `panel`).

```sql
-- Round 13: who gets Maia, and who stays, by group and device
SELECT properties.maia_prompt_variant AS grp, properties.$device_type AS device,
uniqIf(person_id, event IN ('download_model_modal_shown', 'download_model_initiated')) AS needed_maia,
uniqIf(person_id, event = 'download_model_completed') AS got_maia,
uniqIf(person_id, event = 'download_model_dismissed') AS said_not_now,
uniqIf(person_id, event = 'analysis_engine_steps') AS moved_through_a_game
FROM events
WHERE timestamp >= now() - INTERVAL 14 DAY AND properties.maia_prompt_variant IS NOT NULL
GROUP BY grp, device ORDER BY device, grp
```

Not tested: a real phone, whether browsers grant the request to keep the model (the test
browser refused it), and starting a drill on a phone.

## What changed

| commit | change | effect |
Expand Down
36 changes: 28 additions & 8 deletions public/maia-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,17 +110,20 @@ async function storeModel(modelUrl, modelVersion, buffer) {
const tx = db.transaction([STORE_NAME], 'readwrite')
const store = tx.objectStore(STORE_NAME)

// Saved only once the transaction completes: a full disk can still fail it
// after the request itself has succeeded.
await new Promise((resolve, reject) => {
const req = store.put({
tx.oncomplete = () => resolve()
tx.onabort = () => reject(tx.error)
tx.onerror = () => reject(tx.error)
store.put({
id: MODEL_KEY,
url: modelUrl,
version: modelVersion,
data: new Blob([buffer]),
timestamp: Date.now(),
size: buffer.byteLength,
})
req.onsuccess = () => resolve()
req.onerror = () => reject(req.error)
})
}

Expand Down Expand Up @@ -175,10 +178,15 @@ self.onmessage = async (e) => {

if (response.body && typeof response.body.getReader === 'function') {
const reader = response.body.getReader()
const contentLength = +(response.headers.get('Content-Length') || 0)
// A compressed response has no usable length, and the bytes read here
// are decompressed: use the size the page knows.
const contentLength = response.headers.get('Content-Encoding')
? msg.expectedBytes || 0
: +(response.headers.get('Content-Length') || 0)
const chunks = []
let receivedLength = 0
let lastReportedProgress = 0
let lastReportedAt = 0

while (true) {
const { done, value } = await reader.read()
Expand All @@ -187,12 +195,18 @@ self.onmessage = async (e) => {
receivedLength += value.length

if (contentLength > 0) {
const currentProgress = Math.floor(
(receivedLength / contentLength) * 100,
const currentProgress = Math.min(
99,
Math.floor((receivedLength / contentLength) * 100),
)
if (currentProgress >= lastReportedProgress + 10) {
const now = Date.now()
if (
currentProgress > lastReportedProgress &&
now - lastReportedAt >= 250
) {
postMessage({ type: 'progress', progress: currentProgress })
lastReportedProgress = currentProgress
lastReportedAt = now
}
}
}
Expand All @@ -207,7 +221,13 @@ self.onmessage = async (e) => {
buffer = new Uint8Array(await response.arrayBuffer())
}

await storeModel(modelUrl, modelVersion, buffer.buffer)
// Maia still works on this page if saving fails, but it will be
// needed again on the next.
try {
await storeModel(modelUrl, modelVersion, buffer.buffer)
} catch (err) {
postMessage({ type: 'store-failed', message: String(err) })
}
await initSession(buffer.buffer)
postMessage({ type: 'progress', progress: 100 })
postMessage({ type: 'status', status: 'ready' })
Expand Down
23 changes: 21 additions & 2 deletions src/components/Analysis/Highlight.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import {
GameNode,
} from 'src/types'
import { MAIA_MODELS } from 'src/constants/common'
import { WindowSizeContext } from 'src/contexts'
import { MaiaEngineContext, WindowSizeContext } from 'src/contexts'
import {
MAIA_DOWNLOAD_MB,
startMaiaDownload,
} from 'src/lib/engine/maiaDownloadPrompt'

type DescriptionSegment =
| { type: 'text'; content: string }
Expand Down Expand Up @@ -60,6 +64,7 @@ export const Highlight: React.FC<Props> = ({
hideStockfishEvalSummary = false,
hideWhiteWinRateSummary = false,
}: Props) => {
const maiaEngine = useContext(MaiaEngineContext)
const { isMobile } = useContext(WindowSizeContext)

// Check if current position is checkmate (independent of Stockfish
Expand Down Expand Up @@ -576,7 +581,21 @@ export const Highlight: React.FC<Props> = ({
</button>
)
})}
{emptyRows(recommendations.maia?.slice(0, 4).length ?? 0)}
<div className="relative w-full">
{emptyRows(recommendations.maia?.slice(0, 4).length ?? 0)}
{maiaEngine.status === 'no-cache' &&
!recommendations.maia?.length ? (
<button
type="button"
onClick={() =>
startMaiaDownload(maiaEngine.downloadModel, 'panel')
}
className="absolute inset-0 m-auto h-fit rounded border border-glass-border bg-human-4 px-2 py-1 text-xs font-medium text-white"
>
Download Maia-3 ({MAIA_DOWNLOAD_MB}MB)
</button>
) : null}
</div>
</div>
</div>
<div
Expand Down
Loading
Loading