Skip to content
Closed
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
32 changes: 32 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@
--color-vital-hr: var(--chart-vital-hr);
--color-vital-hrv: var(--chart-vital-hrv);
--color-vital-br: var(--chart-vital-br);
--color-calib-prev: var(--calib-prev);
--color-calib-saved: var(--calib-saved);
--color-calib-none: var(--calib-none);
--color-calib-better: var(--calib-better);
--color-actor-agent: var(--actor-agent);
--color-actor-agent-bg: var(--actor-agent-bg);
--color-actor-you: var(--actor-you);
--color-actor-you-bg: var(--actor-you-bg);
--color-actor-pod: var(--actor-pod);
--color-actor-pod-bg: var(--actor-pod-bg);
--radius-tag: 3px;
--radius-thumb: 6px;
--radius-ctl: 8px;
Expand Down Expand Up @@ -162,6 +172,18 @@
--chart-vital-hrv: #b294e6;
--chart-vital-br: #6db3e3;

/* Biometric calibration: previous vs saved profile, and who acted. */
--calib-prev: #3a3a40;
--calib-saved: #7fc0ea;
--calib-none: #2a2a2f;
--calib-better: #5fc27a;
--actor-agent: #b89cf0;
--actor-agent-bg: #231c33;
--actor-you: #7fc0ea;
--actor-you-bg: #12202c;
--actor-pod: #5fc27a;
--actor-pod-bg: #12261a;

--toggle-off-track: var(--sp-line-2);
--toggle-off-knob: #6a6a70;
--toggle-on-knob: var(--sp-ink-0);
Expand Down Expand Up @@ -204,6 +226,16 @@
--chart-vital-hr: #c94f40;
--chart-vital-hrv: #7a5cc4;
--chart-vital-br: #2f7fb8;
--calib-prev: #c4c4c0;
--calib-saved: #2f7fb8;
--calib-none: #e2e2df;
--calib-better: #2e9d5b;
--actor-agent: #6a4fb8;
--actor-agent-bg: #ece6f8;
--actor-you: #2f6fb5;
--actor-you-bg: #e3eef9;
--actor-pod: #2e8a52;
--actor-pod-bg: #e2f3e8;
--toggle-off-track: #dcdcd8;
--toggle-off-knob: #ffffff;
--toggle-on-knob: #ffffff;
Expand Down
2 changes: 1 addition & 1 deletion src/components/Settings/sections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Activity, Cog, Download, Globe, Hand, HardDriveUpload, House, Moon, Pal
export const SECTIONS = [
{ id: 'appearance', label: 'Appearance', icon: Palette, description: 'How this device shows the app' },
{ id: 'backup', label: 'Backup', icon: HardDriveUpload, description: 'Nightly archive to your own storage' },
{ id: 'biometric-calibration', label: 'Biometric calibration', icon: Activity, description: 'Profiles, processing state and evaluation history' },
{ id: 'biometric-calibration', label: 'Biometric calibration', icon: Activity, description: 'Profiles, processing state and evaluation' },
{ id: 'device', label: 'Device', icon: Cog, description: 'Pod-wide hardware and maintenance' },
{ id: 'gestures', label: 'Gestures', icon: Hand, description: 'What taps on the cover do' },
{ id: 'homekit', label: 'HomeKit', icon: House, description: 'Apple Home bridge' },
Expand Down
108 changes: 67 additions & 41 deletions src/components/biometrics/Calibration/BiometricCalibration.tsx
Original file line number Diff line number Diff line change
@@ -1,63 +1,89 @@
'use client'

import { useState } from 'react'
import { ChevronLeft } from 'lucide-react'
import { Trans, useLingui } from '@lingui/react/macro'
import { Button, Card } from '@/src/components/ds'
import { SideTabs } from '@/src/components/Settings/SettingsLayout'
import { SleepStageRules } from '@/src/components/Settings/SleepStageRules'
import { CalibrationOverview } from './CalibrationOverview'
import { cn } from '@/lib/utils'
import { CalibrationReferences } from './CalibrationReferences'
import { ReferenceLibrary } from './ReferenceLibrary'
import { ProfileLibrary } from './ProfileLibrary'
import { ProfileRecovery } from './ProfileRecovery'
import { CalibrationBacktests } from './CalibrationBacktests'
import { CalibrationHistory } from './CalibrationHistory'
import { ShadowRuns } from './ShadowRuns'
import { CalibrationPage } from './CalibrationPage'
import { SideTabs } from '@/src/components/Settings/SettingsLayout'

type Side = 'left' | 'right'
type View = 'overview' | 'references' | 'profiles' | 'backtests' | 'observations'

/**
* The calibration page is the overview. The workbench views behind it —
* reference imports, profile revisions, datasets and backtests, prospective
* runs — stay one click away for the cases the page links out to.
*/
export function BiometricCalibration({ side, onSideChange }: { side: Side, onSideChange: (side: Side) => void }) {
const { t } = useLingui()
const [view, setView] = useState<'overview' | 'references' | 'backtests' | 'profiles' | 'history' | 'observations'>('overview')
const [view, setView] = useState<View>('overview')
const views: Array<{ id: View, label: string }> = [
{ id: 'overview', label: t`Overview` },
{ id: 'references', label: t`References` },
{ id: 'profiles', label: t`Profiles` },
{ id: 'backtests', label: t`Backtests` },
{ id: 'observations', label: t`Observations` },
]
const nav = (
<nav aria-label={t`Calibration views`} className="flex flex-wrap items-center gap-x-3.5 gap-y-1 text-[13px]">
{view !== 'overview' && (
<button type="button" onClick={() => setView('overview')} className="-ml-1 flex cursor-pointer items-center gap-0.5 border-0 bg-transparent p-0 text-fg-2 hover:text-fg">
<ChevronLeft size={16} />
<Trans>Calibration</Trans>
</button>
)}
<span className="font-mono text-[11px] tracking-[0.08em] text-fg-3">{t`WORKBENCH`}</span>
{views.map(v => (
<button
key={v.id}
type="button"
aria-pressed={view === v.id}
onClick={() => setView(v.id)}
className={cn('cursor-pointer border-0 bg-transparent p-0', view === v.id ? 'text-fg' : 'text-fg-2 hover:text-fg', v.id === 'overview' && view === 'overview' && 'hidden')}
>
{v.label}
</button>
))}
</nav>
)

if (view === 'overview') {
return (
<div className="flex flex-col gap-5">
<CalibrationPage key={side} side={side} onSideChange={onSideChange} onOpenTool={setView} />
{nav}
</div>
)
}
return (
<div className="space-y-4">
<Card className="space-y-3">
<div className="flex flex-col gap-4">
{nav}
<div className="max-w-xs">
<SideTabs side={side} onChange={onSideChange} label={t`Biometric calibration`} />
<nav aria-label={t`Calibration views`} className="flex flex-wrap gap-2">
<Button aria-pressed={view === 'overview'} onClick={() => setView('overview')}><Trans>Overview</Trans></Button>
<Button aria-pressed={view === 'references'} onClick={() => setView('references')}><Trans>References</Trans></Button>
<Button aria-pressed={view === 'profiles'} onClick={() => setView('profiles')}><Trans>Profiles</Trans></Button>
<Button aria-pressed={view === 'backtests'} onClick={() => setView('backtests')}><Trans>Backtests</Trans></Button>
<Button aria-pressed={view === 'history'} onClick={() => setView('history')}><Trans>History</Trans></Button>
<Button aria-pressed={view === 'observations'} onClick={() => setView('observations')}><Trans>Observations</Trans></Button>
</nav>
</Card>
{view === 'overview'
</div>
{view === 'references'
? (
<div key={side} className="space-y-4">
<CalibrationOverview side={side} />
<ProfileRecovery side={side} />
<CalibrationReferences side={side} />
<ReferenceLibrary side={side} />
</div>
)
: view === 'references'
? (
<div key={side} className="space-y-4">
<CalibrationReferences side={side} />
<ReferenceLibrary side={side} />
</div>
)
: view === 'backtests'
? <CalibrationBacktests key={side} side={side} />
: view === 'observations'
? <ShadowRuns key={side} side={side} />
: view === 'history'
? <CalibrationHistory key={side} side={side} />
: (
<>
<ProfileLibrary key={side} side={side} />
<p className="text-sm text-fg-2"><Trans>Direct sleep-stage changes are unvalidated manual settings. Saving requests a profile change; processor adoption is reported separately in Overview.</Trans></p>
<SleepStageRules side={side} onSideChange={onSideChange} />
</>
)}
: view === 'backtests'
? <CalibrationBacktests key={side} side={side} />
: view === 'observations'
? <ShadowRuns key={side} side={side} />
: (
<>
<ProfileLibrary key={side} side={side} />
<p className="text-sm text-fg-2"><Trans>Profile revisions here are candidates. Applying one requests it like Save and request does; the overview shows when the processor acknowledges it.</Trans></p>
</>
)}
</div>
)
}
47 changes: 47 additions & 0 deletions src/components/biometrics/Calibration/CalibrationActivity.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use client'

import { useState } from 'react'
import { useLingui } from '@lingui/react/macro'
import { Card } from '@/src/components/ds'
import type { Actor } from './calibration-model'
import { ActorBadge } from './CalibrationProfileCard'

export interface ActivityRow { id: string, atMs: number, actor: Actor, what: string, detail: string | null, when?: string }

const VISIBLE = 6

/** Who did what to this side's profile, newest first. */
export function ActivityCard({ rows, timezone }: { rows: ActivityRow[], timezone: string }) {
const { t } = useLingui()
const [all, setAll] = useState(false)
const shown = all ? rows : rows.slice(0, VISIBLE)
return (
<Card className="gap-1">
<div className="mb-2 flex items-baseline justify-between">
<span className="text-[15px] font-medium">{t`Activity`}</span>
<span className="text-xs text-fg-2">{timezone}</span>
</div>
{rows.length === 0 && <p className="border-t border-line py-2.5 text-[13px] text-fg-2">{t`Nothing has happened on this side yet.`}</p>}
<ol className="m-0 flex list-none flex-col p-0">
{shown.map(row => (
<li key={row.id} className="flex flex-col gap-1 border-t border-line py-2.5">
<div className="flex items-baseline gap-2 text-[13px]">
<ActorBadge kind={row.actor.kind} />
<span className="min-w-0 flex-1">
{row.actor.kind === 'agent' && <span className="text-fg-2">{`${row.actor.name} `}</span>}
{row.what}
</span>
<span className="whitespace-nowrap font-mono text-[11px] text-fg-3">{row.when}</span>
</div>
{row.detail && <div className="pl-[54px] font-mono text-xs text-fg-2">{row.detail}</div>}
</li>
))}
</ol>
{rows.length > VISIBLE && (
<button type="button" onClick={() => setAll(!all)} className="cursor-pointer self-start border-0 bg-transparent p-0 pt-1 text-xs text-fg-2 hover:text-fg">
{all ? t`Show recent` : t`Show all ${rows.length}`}
</button>
)}
</Card>
)
}
Loading
Loading