Files
hanzo.ai/components/kai/Surface.tsx
zeekayandHanzo Dev 2d8ddd097d /kai: theme inks for the figures, a phone-sized headline, plain board title
The distribution's track and runners-up, the switches and the surface read in
theme inks, so they hold on a light ground. The hero's sentence takes a size
that tracks the width on phones, keeping "don't generate text." on one line at
375. The harness board says what it is: Kai and Jev on Decision Index v2.
evidence_sufficient is evidence_enough, so the graph's label clears its value.


Co-authored-by: Hanzo Dev <[email protected]>
2026-10-09 04:27:02 -07:00

109 lines
4.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
/**
* See what changes the answer: one decision drawn over two of the facts it reads,
* with the line where the answer turns over, and the smallest change to the
* evidence that would turn it.
*
* AN EXAMPLE, and the bar says so. The surface is a stated rule — continue once
* the evidence clears the risk by fifteen points — so the same sliders always
* draw the same answer; it shows what a bounded decision looks like from the
* outside, not a run of the model.
*/
import { useState, type ChangeEvent } from 'react'
import { Text, View, XStack, YStack } from '@hanzo/ui'
import { useTween } from '@/components/motion'
import { Dist, Lines, Panel } from './figures'
/** Continue once the evidence clears the risk by this much. */
const MARGIN = 15
const p = (e: number, r: number) => 1 / (1 + Math.exp(-(e - r - MARGIN) / 8))
const S = 240 // the square, in viewBox units
const x = (e: number) => (e / 100) * S
const y = (r: number) => S - (r / 100) * S
function Slider({ label, value, on }: { label: string; value: number; on: (v: number) => void }) {
return (
<YStack gap="$1">
<XStack justify="space-between">
<Text fontFamily="$mono" fontSize="$1" color="var(--text-tertiary)">
{label}
</Text>
<Text fontFamily="$mono" fontSize="$1" color="$ink" $platform-web={{ fontVariantNumeric: 'tabular-nums' }}>
{value}%
</Text>
</XStack>
<View
render={
<input
type="range"
min={0}
max={100}
value={value}
aria-label={label}
onChange={(ev: ChangeEvent<HTMLInputElement>) => on(Number(ev.target.value))}
/>
}
width="100%"
$platform-web={{ accentColor: 'var(--foreground)' }}
/>
</YStack>
)
}
export default function Surface() {
const [evidence, setEvidence] = useState(55)
const [risk, setRisk] = useState(50)
const go = p(evidence, risk)
const [cont, rev] = useTween([go, 1 - go], { ms: 260 })
// Continue holds from the edge up: leaving it takes one point under the edge,
// reaching it takes the edge itself, and past 100 the edge cannot be reached.
const edge = risk + MARGIN
const continuing = evidence >= edge
const flip = continuing ? evidence - edge + 1 : edge - evidence
const reach = continuing || edge <= 100
return (
<YStack data-figure="surface" gap="$4" width="100%" maxW={560} mx="auto">
<Panel title="what flips the decision">
<XStack gap="$5" flexWrap="wrap" items="flex-start">
<View width="100%" maxW={260} mx="auto" $sm={{ width: 240, mx: 0 }} shrink={0}>
<svg viewBox={`-30 -8 ${S + 38} ${S + 40}`} role="img" aria-label={`Evidence ${evidence} percent against risk ${risk} percent: ${continuing ? 'continue' : 'review'}`}>
<path d={`M${x(MARGIN)},${y(0)} L${x(100)},${y(100 - MARGIN)} L${x(100)},${y(0)} Z`} fill="var(--border)" />
<rect x={0} y={0} width={S} height={S} fill="none" stroke="var(--text-disabled)" />
<path d={`M${x(MARGIN)},${y(0)} L${x(100)},${y(100 - MARGIN)}`} stroke="var(--text-secondary)" strokeWidth={1.5} strokeDasharray="5 4" />
<text x={x(70)} y={y(14)} fontSize={15} fill="var(--text-secondary)" textAnchor="middle" fontFamily="var(--font-mono)">
continue
</text>
<text x={x(30)} y={y(80)} fontSize={15} fill="var(--text-secondary)" textAnchor="middle" fontFamily="var(--font-mono)">
review
</text>
<text x={S / 2} y={S + 26} fontSize={13} fill="var(--text-tertiary)" textAnchor="middle" fontFamily="var(--font-mono)">
evidence →
</text>
<text x={-16} y={S / 2} fontSize={13} fill="var(--text-tertiary)" textAnchor="middle" fontFamily="var(--font-mono)" transform={`rotate(-90 -16 ${S / 2})`}>
risk →
</text>
<circle cx={x(evidence)} cy={y(risk)} r={8} fill="var(--foreground)" />
</svg>
</View>
<YStack flex={1} minW={200} gap="$3">
<Dist rows={[['continue', cont], ['review', rev]]} lead={continuing ? 0 : 1} />
<Lines lines={['', ['what flips it', 'dim']]} />
<Text display="block" color="$ink" aria-live="polite" data-flip>
{reach
? `${continuing ? '−' : '+'}${flip} ${flip === 1 ? 'point' : 'points'} of evidence turns ${continuing ? 'continue into review' : 'review into continue'}.`
: 'No amount of evidence clears this risk. It stays with review.'}
</Text>
</YStack>
</XStack>
</Panel>
<YStack gap="$3">
<Slider label="evidence completeness" value={evidence} on={setEvidence} />
<Slider label="risk" value={risk} on={setRisk} />
</YStack>
</YStack>
)
}