83 lines
2.7 KiB
TypeScript
83 lines
2.7 KiB
TypeScript
"use client"
|
|
|
|
import { CheckCircle2, AlertTriangle, Loader2 } from "lucide-react"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
type TimelineItem = {
|
|
key: string
|
|
label: string
|
|
status: "pending" | "running" | "completed" | "failed"
|
|
detail?: string
|
|
}
|
|
|
|
function StatusIcon({ status }: { status: TimelineItem["status"] }) {
|
|
if (status === "completed") {
|
|
return <CheckCircle2 className="h-4 w-4 text-foreground" />
|
|
}
|
|
if (status === "failed") {
|
|
return <AlertTriangle className="h-4 w-4 text-destructive" />
|
|
}
|
|
if (status === "running") {
|
|
return <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
|
}
|
|
return <span className="h-2.5 w-2.5 rounded-full border border-muted-foreground/60" />
|
|
}
|
|
|
|
export function AnalysisTimeline({ items }: { items: TimelineItem[] }) {
|
|
if (!items.length) return null
|
|
|
|
return (
|
|
<div className="rounded-lg border border-border/60 bg-muted/20 p-4">
|
|
<div className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
Analysis timeline
|
|
</div>
|
|
<div className="mt-3 space-y-3">
|
|
{items.map((item, index) => {
|
|
const isPending = item.status === "pending"
|
|
const nextStatus = items[index + 1]?.status
|
|
const isStrongLine =
|
|
nextStatus &&
|
|
(item.status === "completed" || item.status === "running") &&
|
|
(nextStatus === "completed" || nextStatus === "running")
|
|
return (
|
|
<div key={item.key} className="relative pl-6">
|
|
<span
|
|
className={cn(
|
|
"absolute left-[6px] top-3 bottom-[-12px] w-px",
|
|
index === items.length - 1 ? "hidden" : "bg-border/40",
|
|
isStrongLine && "bg-foreground/60"
|
|
)}
|
|
/>
|
|
<div
|
|
className={cn(
|
|
"flex items-start gap-3 text-sm transition",
|
|
isPending && "scale-[0.96] opacity-60"
|
|
)}
|
|
>
|
|
<div className="mt-0.5 flex h-5 w-5 items-center justify-center rounded-full bg-background shadow-sm">
|
|
<StatusIcon status={item.status} />
|
|
</div>
|
|
<div className="min-w-0">
|
|
<div
|
|
className={cn(
|
|
"font-medium",
|
|
item.status === "failed" && "text-destructive"
|
|
)}
|
|
>
|
|
{item.label}
|
|
</div>
|
|
{item.detail && (
|
|
<div className="text-xs text-muted-foreground">
|
|
{item.detail}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|