PawTrace / frontend /src /components /SearchProgress.tsx
Elliott Duke
Demo: Dobby hero, photo swaps, revised copy, softened accuracy figures
6d6ee02
Raw
History Blame Contribute Delete
2.85 kB
// Animated progress for a photo search. The search is a single server call (embed -> search -> rank
// all happen server-side in ~1s), so this walks through the real stages on a short timer with an
// indeterminate bar, and surfaces how many dogs the photo is being compared against.
import { useEffect, useState } from "react";
// Minimum time the progress panel stays up, so the staged steps play out at a readable pace even
// when the search itself returns in a fraction of a second. The callers await this before showing
// results.
export const MIN_PROGRESS_MS = 1600;
export default function SearchProgress({
mode = "match",
count,
}: {
mode?: "match" | "breed";
count?: number;
}) {
const dogs = count && count > 0 ? count.toLocaleString() : null;
const steps =
mode === "breed"
? ["Reading the image", "Classifying across 120 breeds", "Ranking the closest look-alikes"]
: [
"Extracting features",
dogs ? `Searching ${dogs} dogs` : "Searching the database",
"Ranking the closest matches",
];
const subtitle =
mode === "breed"
? "Classifying against 120 known breeds"
: dogs
? `Comparing against ${dogs} found & unclaimed dogs`
: null;
const [step, setStep] = useState(0);
useEffect(() => {
const id = setInterval(() => setStep((s) => Math.min(s + 1, steps.length - 1)), 600);
return () => clearInterval(id);
}, [steps.length]);
const pct = [34, 68, 95][step];
return (
<div className="card" role="status" aria-live="polite">
<div className="flex items-center gap-3">
<span
className="h-5 w-5 shrink-0 rounded-full border-2 border-leaf-200 border-t-leaf-600 animate-spin motion-reduce:animate-none"
aria-hidden
/>
<div className="min-w-0 flex-1">
<p className="font-semibold text-sm">{steps[step]}…</p>
{subtitle && <p className="text-xs text-gray-500">{subtitle}</p>}
</div>
</div>
<div className="mt-3 h-2 rounded-full bg-gray-100 overflow-hidden">
<div
className="h-full rounded-full bg-leaf-500 transition-all duration-500 ease-out"
style={{ width: `${pct}%` }}
/>
</div>
<ol className="mt-3 grid gap-1">
{steps.map((s, i) => (
<li
key={s}
className={`flex items-center gap-2 text-xs ${
i < step ? "text-leaf-700" : i === step ? "text-leaf-700 font-medium" : "text-gray-400"
}`}
>
<span
aria-hidden
className={`h-2 w-2 shrink-0 rounded-full border ${
i <= step ? "bg-leaf-500 border-leaf-500" : "bg-transparent border-gray-300"
}`}
/>
{s}
</li>
))}
</ol>
</div>
);
}