mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
The lead's tiles and the landing's draw at once when every read has answered or failed (useSettled); the lead keeps its places at their final height until then, so its layout shift falls from 0.17-0.42 to under 0.01. Gates that open /benchmarks, /kai and /templates wait for html[data-hydrated] before their timed waits, and the console-clean gate answers /v1/event, so a starved runner's boot no longer spends their budgets. Co-authored-by: Hanzo Dev <[email protected]>
131 lines
5.3 KiB
TypeScript
131 lines
5.3 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useMemo, useRef } from 'react'
|
|
import { ready, usePricing, useServed, type Reading } from '@/lib/api/use'
|
|
import { table, type Claim, type Kai, type Prices, type Table } from '@/lib/kai'
|
|
import { JEV, kaiLead, type Tile } from '@/lib/tally'
|
|
import { useEnso } from './useEnso'
|
|
import { useKai } from './useKai'
|
|
|
|
/**
|
|
* The Decision Suite as a page draws it: its rows from the research runs
|
|
* `useKai` reads, its cost row from the model list (`GET /v1/models`), both read
|
|
* when the page opens. The suite waits on the runs alone: until the model list
|
|
* answers, or if it does not, the cost row prints a dash and the board states no
|
|
* count.
|
|
*/
|
|
export function useSuite(): Reading<Table> {
|
|
const kai = useKai()
|
|
const models = ready(useServed())
|
|
const rate = (id: string) => models?.find((m) => m.id === id)?.pricing?.input_per_million ?? null
|
|
const k = models ? rate('kai') : undefined
|
|
const j = models ? rate(JEV) : undefined
|
|
return useMemo((): Reading<Table> => {
|
|
if (kai.state !== 'ready') return kai
|
|
const prices: Prices | null = k === undefined || j === undefined ? null : { kai: k, jev: j }
|
|
return { state: 'ready', data: table(kai.data.suite, prices) }
|
|
}, [kai, k, j])
|
|
}
|
|
|
|
/**
|
|
* The Decision Suite's claim and the runs it was drawn from, once both reads it
|
|
* turns on have settled: the runs read, and the model list answered or failed.
|
|
* Null until then, so nothing that leads with Kai states the harness count in
|
|
* the moment before the suite's. The claim is null when the suite does not hold.
|
|
*/
|
|
export function useClaim(): { claim: Claim | null; kai: Kai } | null {
|
|
const kai = useKai()
|
|
const suite = useSuite()
|
|
const listed = useServed().state !== 'loading'
|
|
return useMemo(
|
|
() => (kai.state === 'ready' && suite.state === 'ready' && listed ? { claim: suite.data.claim, kai: kai.data } : null),
|
|
[kai, suite, listed],
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Whether every read the headline figures turn on has answered or failed: the
|
|
* research runs (Enso's and Kai's), the model list (through `useClaim`) and the
|
|
* price list. The landing's tiles and /benchmarks' lead wait on it and then draw
|
|
* at once, so no figure moves another once drawn.
|
|
*/
|
|
export function useSettled(): boolean {
|
|
const runs = useKai().state
|
|
const enso = useEnso().state
|
|
const priced = usePricing().state !== 'loading'
|
|
const said = useClaim()
|
|
return priced && enso !== 'loading' && (said !== null || runs === 'unreachable')
|
|
}
|
|
|
|
/**
|
|
* Kai's headline figure as every page that leads with it draws it (lib/tally
|
|
* `kaiLead`): the Decision Suite's count while it holds, else the harness
|
|
* suites Kai leads; nothing until `useClaim` has settled, or when the runs
|
|
* cannot be read.
|
|
*/
|
|
export function useLead(): Tile | null {
|
|
const c = useClaim()
|
|
return useMemo(() => (c ? kaiLead(c.claim, c.kai) : null), [c])
|
|
}
|
|
|
|
/** How long, in milliseconds from the moment every read has answered, the anchor is held at the top while the sections above it settle. */
|
|
const HOLD = 1500
|
|
|
|
/** What counts as the reader moving the page themselves. */
|
|
const MOVES = ['wheel', 'touchstart', 'keydown', 'pointerdown'] as const
|
|
|
|
/** The address's anchor as an id: the hash decoded where it is well formed, as written where it is not. */
|
|
function anchor(): string {
|
|
const raw = window.location.hash.slice(1)
|
|
try {
|
|
return decodeURIComponent(raw)
|
|
} catch {
|
|
return raw
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Brings the address's anchor back to the top once the page has stopped
|
|
* growing. A link from another page (the landing's Kai tile) arrives before
|
|
* anything is read, and the sections above its target grow as the runs, the
|
|
* model list and the price list answer. Once all three have, and `drawn` says
|
|
* the runs' sections are drawn, the anchor is scrolled to at once, unless the
|
|
* reader has moved the page in the meantime: by wheel, touch, key or press
|
|
* once the page runs, or before it ran, past the anchor.
|
|
*/
|
|
export function useAnchor(drawn: boolean): void {
|
|
const listed = useServed().state !== 'loading'
|
|
const priced = usePricing().state !== 'loading'
|
|
const moved = useRef(false)
|
|
useEffect(() => {
|
|
// The browser's own jump to the anchor is a smooth scroll still under way when the page starts, so only a reader
|
|
// already past the anchor is known to have moved before it ran; an anchor with no scroll margin lands a fraction
|
|
// of a pixel above the top, which is the jump and not the reader.
|
|
const at = document.getElementById(anchor())?.getBoundingClientRect().top
|
|
if (at != null && at < -1) moved.current = true
|
|
const move = () => {
|
|
moved.current = true
|
|
}
|
|
for (const e of MOVES) window.addEventListener(e, move, { passive: true, once: true })
|
|
return () => {
|
|
for (const e of MOVES) window.removeEventListener(e, move)
|
|
}
|
|
}, [])
|
|
const settled = drawn && listed && priced
|
|
useEffect(() => {
|
|
if (!settled || moved.current) return
|
|
const go = () => {
|
|
if (!moved.current) document.getElementById(anchor())?.scrollIntoView({ block: 'start', behavior: 'instant' })
|
|
}
|
|
go()
|
|
// A chart or a table above it can still re-measure in the frames after; the anchor is held through them.
|
|
const watch = new ResizeObserver(go)
|
|
watch.observe(document.body)
|
|
const done = setTimeout(() => watch.disconnect(), HOLD)
|
|
return () => {
|
|
clearTimeout(done)
|
|
watch.disconnect()
|
|
}
|
|
}, [settled])
|
|
}
|