Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
import { useStore } from '@nanostores/react'
|
||||
import clsx from 'clsx'
|
||||
import { Check, ChevronRight, FileText, X } from 'lucide-react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
import { BrandMark } from '../components/brand-mark'
|
||||
import { Button } from '../components/button'
|
||||
import { Loader } from '../components/loader'
|
||||
import { formatDuration, formatElapsed } from '../lib/format'
|
||||
import {
|
||||
$mode,
|
||||
$progress,
|
||||
type BootstrapStateModel,
|
||||
cancelInstall,
|
||||
type StageState
|
||||
} from '../store'
|
||||
|
||||
interface ProgressProps {
|
||||
bootstrap: BootstrapStateModel
|
||||
}
|
||||
|
||||
/*
|
||||
* Progress screen — drives a stage list + collapsible log panel. Uses
|
||||
* the DS <Progress> for the top bar so its motion + ring match the rest
|
||||
* of the product.
|
||||
*/
|
||||
export default function ProgressScreen({ bootstrap }: ProgressProps) {
|
||||
const progress = useStore($progress)
|
||||
const mode = useStore($mode)
|
||||
const [showLogs, setShowLogs] = useState(false)
|
||||
const [now, setNow] = useState(() => Date.now())
|
||||
const logEndRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (showLogs && logEndRef.current) {
|
||||
logEndRef.current.scrollIntoView({ behavior: 'smooth' })
|
||||
}
|
||||
}, [bootstrap.logs.length, showLogs])
|
||||
|
||||
// Tick once a second while the run is in flight so the active step shows a
|
||||
// live elapsed timer — a long single step (e.g. the dependency download)
|
||||
// reads as working, not frozen. Stops when nothing is running.
|
||||
useEffect(() => {
|
||||
if (bootstrap.status !== 'running') {
|
||||
return
|
||||
}
|
||||
|
||||
const id = window.setInterval(() => setNow(Date.now()), 1000)
|
||||
|
||||
return () => window.clearInterval(id)
|
||||
}, [bootstrap.status])
|
||||
|
||||
const isUpdate = mode === 'update'
|
||||
const title = bootstrap.status === 'completed' ? 'Done' : isUpdate ? 'Updating Hermes' : 'Setting up Hermes Agent'
|
||||
|
||||
const description = isUpdate
|
||||
? 'Hermes is updating to the latest version — this only takes a moment.'
|
||||
: 'This is a one-time setup. The Hermes installer is downloading dependencies and configuring your machine. Subsequent launches will skip this step.'
|
||||
|
||||
const pct = Math.round(progress.fraction * 100)
|
||||
|
||||
return (
|
||||
<div className="hermes-fade-in flex h-full flex-col">
|
||||
{/* Header: brand + title + description, matching the desktop install overlay. */}
|
||||
<div className="flex shrink-0 items-start gap-4 px-6 pt-6 pb-4">
|
||||
<BrandMark className="size-11" />
|
||||
<div className="min-w-0">
|
||||
<h2 className="text-xl font-semibold tracking-tight">{title}</h2>
|
||||
<p className="mt-1.5 text-sm text-muted-foreground">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
<div className="flex-1 overflow-y-auto px-6 pt-2 pb-4">
|
||||
{/* Progress line + bar; the count shimmers while the install runs.
|
||||
pt-2 matches the log header's py-2 so the "steps complete" line and
|
||||
the "Live output" header share a baseline. */}
|
||||
<div className="mb-4">
|
||||
<div className="mb-1 flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span className={clsx(bootstrap.status === 'running' && 'shimmer')}>
|
||||
{progress.done} of {progress.total} steps complete
|
||||
</span>
|
||||
<span className="tabular-nums">{pct}%</span>
|
||||
</div>
|
||||
<div className="h-1.5 w-full overflow-hidden rounded-full bg-(--ui-bg-tertiary)">
|
||||
<div
|
||||
className="h-full bg-primary transition-all duration-300 ease-out"
|
||||
style={{ width: `${Math.max(2, progress.fraction * 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Flat stage list: only the running step is opaque; the rest read as
|
||||
muted. Running loader overhangs left so labels stay aligned; the
|
||||
terminal check/cross sits right of the label. */}
|
||||
<ol className="space-y-0.5">
|
||||
{bootstrap.stageOrder.map((name) => {
|
||||
const rec = bootstrap.stages[name]
|
||||
|
||||
if (!rec) {return null}
|
||||
|
||||
const meta =
|
||||
rec.state === 'running' && rec.startedAt != null
|
||||
? formatElapsed(now - rec.startedAt)
|
||||
: rec.durationMs != null && rec.state !== 'failed'
|
||||
? formatDuration(rec.durationMs)
|
||||
: null
|
||||
|
||||
return (
|
||||
<li
|
||||
className={clsx(
|
||||
'flex items-center gap-2.5 px-3 py-1.5 text-sm',
|
||||
rec.state === 'running'
|
||||
? 'font-medium text-foreground'
|
||||
: 'text-muted-foreground'
|
||||
)}
|
||||
key={name}
|
||||
>
|
||||
{rec.state === 'running' && <Loader className="-ml-2 size-6 shrink-0" />}
|
||||
<span className="flex-1 truncate">{rec.info.title}</span>
|
||||
{meta && <span className="text-xs tabular-nums text-muted-foreground/70">{meta}</span>}
|
||||
<StateIcon state={rec.state ?? null} />
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
{showLogs && (
|
||||
<div className="flex w-1/2 flex-col border-l border-(--stroke-nous)">
|
||||
<div className="flex shrink-0 items-center justify-between border-b border-(--stroke-nous) px-3 py-2 text-xs">
|
||||
<span className="font-medium text-foreground/80">Live output</span>
|
||||
<span className="tabular-nums text-muted-foreground">{bootstrap.logs.length} lines</span>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto px-3 py-2 font-mono text-[10.5px] leading-relaxed">
|
||||
{bootstrap.logs.map((entry, idx) => (
|
||||
<div
|
||||
className={clsx(
|
||||
'whitespace-pre-wrap',
|
||||
entry.stream === 'stderr' ? 'text-foreground/45' : 'text-foreground/70'
|
||||
)}
|
||||
key={idx}
|
||||
>
|
||||
{entry.line}
|
||||
</div>
|
||||
))}
|
||||
<div ref={logEndRef} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 items-center justify-between border-t border-(--stroke-nous) px-6 py-3">
|
||||
<button
|
||||
className="inline-flex cursor-pointer items-center gap-1.5 text-xs text-muted-foreground transition-colors hover:text-foreground"
|
||||
onClick={() => setShowLogs((v) => !v)}
|
||||
type="button"
|
||||
>
|
||||
<FileText size={14} />
|
||||
{showLogs ? 'Hide details' : 'Show details'}
|
||||
<ChevronRight className={clsx('transition-transform', showLogs && 'rotate-90')} size={12} />
|
||||
</button>
|
||||
|
||||
{bootstrap.status === 'running' && (
|
||||
<Button onClick={() => void cancelInstall()} size="sm" variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Terminal-state markers, neutral by design: a muted check for done/skipped
|
||||
// (no celebratory green), a destructive cross for failure. Running renders its
|
||||
// spinner on the left; pending stays icon-less.
|
||||
function StateIcon({ state }: { state: StageState | null }) {
|
||||
if (state === 'succeeded') {
|
||||
return <Check className="shrink-0 text-muted-foreground" size={13} />
|
||||
}
|
||||
|
||||
if (state === 'skipped') {
|
||||
return <Check className="shrink-0 text-muted-foreground/50" size={13} />
|
||||
}
|
||||
|
||||
if (state === 'failed') {
|
||||
return <X className="shrink-0 text-destructive" size={13} />
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user