mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-10 08:32:09 +00:00
fix(desktop): mount onboarding from frame 1 to kill the FOUT
Default onboarding.configured to null (unknown until the runtime check resolves) and have the onboarding overlay render whenever it's not yet confirmed true. The boot overlay now yields to it, so the very first paint is the Welcome card with a "While we get you set up..." progress strip instead of a flash of the chat shell between boot dismiss and onboarding mount. The picker swaps in cleanly once the gateway opens and the runtime check confirms the user is not configured. Already-configured users see the same prep card briefly while their existing runtime warms up, then the overlay dismisses without touching the chat shell.
This commit is contained in:
parent
11d04d9d5e
commit
2d0aa1b7cb
3 changed files with 58 additions and 5 deletions
|
|
@ -3,9 +3,18 @@ import { useStore } from '@nanostores/react'
|
|||
import { Loader } from '@/components/ui/loader'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { $desktopBoot } from '@/store/boot'
|
||||
import { $desktopOnboarding } from '@/store/onboarding'
|
||||
|
||||
export function DesktopBootOverlay() {
|
||||
const boot = useStore($desktopBoot)
|
||||
const onboarding = useStore($desktopOnboarding)
|
||||
|
||||
// Onboarding overlay covers the whole "first run" UX: it mounts from frame 1
|
||||
// and renders boot progress inline. Yield to it whenever it has not yet
|
||||
// confirmed the user is configured.
|
||||
if (onboarding.configured !== true) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!boot.visible) {
|
||||
return null
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { Button } from '@/components/ui/button'
|
|||
import { Input } from '@/components/ui/input'
|
||||
import { Check, ChevronLeft, ChevronRight, ExternalLink, KeyRound, Loader2, Sparkles } from '@/lib/icons'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { $desktopBoot, type DesktopBootState } from '@/store/boot'
|
||||
import {
|
||||
$desktopOnboarding,
|
||||
cancelOnboardingFlow,
|
||||
|
|
@ -107,7 +108,7 @@ const sortProviders = (providers: OAuthProvider[]) =>
|
|||
|
||||
export function DesktopOnboardingOverlay({ enabled, onCompleted, requestGateway }: DesktopOnboardingOverlayProps) {
|
||||
const onboarding = useStore($desktopOnboarding)
|
||||
const visible = (enabled || onboarding.requested) && !onboarding.configured
|
||||
const boot = useStore($desktopBoot)
|
||||
const ctxRef = useRef<OnboardingContext>({ requestGateway, onCompleted })
|
||||
ctxRef.current = { requestGateway, onCompleted }
|
||||
|
||||
|
|
@ -125,23 +126,64 @@ export function DesktopOnboardingOverlay({ enabled, onCompleted, requestGateway
|
|||
}
|
||||
}, [ctx, enabled, onboarding.requested])
|
||||
|
||||
if (!visible) {
|
||||
// Mount from frame 1 so we replace the boot overlay seamlessly. The
|
||||
// configured field stays null until the runtime check resolves; only then
|
||||
// do we know whether to dismiss (true) or surface the picker (false).
|
||||
if (onboarding.configured === true) {
|
||||
return null
|
||||
}
|
||||
|
||||
const { flow } = onboarding
|
||||
const ready = enabled && onboarding.configured === false
|
||||
const showPicker = flow.status === 'idle' || flow.status === 'success'
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-1300 flex items-center justify-center bg-background/80 p-6 backdrop-blur-xl">
|
||||
<div className="w-full max-w-2xl overflow-hidden rounded-3xl border border-border bg-card/95 shadow-2xl">
|
||||
<Header />
|
||||
<div className="grid gap-5 p-6">{showPicker ? <Picker ctx={ctx} /> : <FlowPanel ctx={ctx} flow={flow} />}</div>
|
||||
<div className="grid gap-5 p-6">
|
||||
{ready ? (
|
||||
showPicker ? (
|
||||
<Picker ctx={ctx} />
|
||||
) : (
|
||||
<FlowPanel ctx={ctx} flow={flow} />
|
||||
)
|
||||
) : (
|
||||
<Preparing boot={boot} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Preparing({ boot }: { boot: DesktopBootState }) {
|
||||
const progress = Math.max(2, Math.min(100, Math.round(boot.progress)))
|
||||
const hasError = Boolean(boot.error)
|
||||
|
||||
return (
|
||||
<div className="grid gap-3" role="status">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
While we get you set up — Hermes is finishing install. This usually takes under a minute on first run.
|
||||
</p>
|
||||
<div className="h-2 overflow-hidden rounded-full bg-muted">
|
||||
<div
|
||||
className={cn(
|
||||
'h-full rounded-full bg-primary transition-[width] duration-300 ease-out',
|
||||
hasError && 'bg-destructive'
|
||||
)}
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3 text-xs text-muted-foreground">
|
||||
<span className="truncate">{boot.message}</span>
|
||||
<span>{progress}%</span>
|
||||
</div>
|
||||
{hasError ? <p className="text-xs text-destructive">{boot.error}</p> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Header() {
|
||||
return (
|
||||
<div className="border-b border-border bg-muted/30 px-6 py-5">
|
||||
|
|
|
|||
|
|
@ -27,7 +27,9 @@ export type OnboardingFlow =
|
|||
| { message: string; provider?: OAuthProvider; start?: OAuthStartResponse; status: 'error' }
|
||||
|
||||
export interface DesktopOnboardingState {
|
||||
configured: boolean
|
||||
/** null until the first runtime check resolves; lets the overlay render
|
||||
* during boot without flickering for already-configured users. */
|
||||
configured: boolean | null
|
||||
flow: OnboardingFlow
|
||||
mode: OnboardingMode
|
||||
providers: null | OAuthProvider[]
|
||||
|
|
@ -41,7 +43,7 @@ export interface OnboardingContext {
|
|||
}
|
||||
|
||||
const INITIAL: DesktopOnboardingState = {
|
||||
configured: true,
|
||||
configured: null,
|
||||
flow: { status: 'idle' },
|
||||
mode: 'oauth',
|
||||
providers: null,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue