feat: add progress tracker component

This commit is contained in:
2001-01-01 00:00:00 +00:00
parent 7f80cf5868
commit 618055be6b

View File

@@ -0,0 +1,211 @@
"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
import type { GenerationStatus } from "@codeboard/shared";
import {
Loader2,
CheckCircle2,
Circle,
AlertCircle,
FileText,
RefreshCw,
} from "lucide-react";
interface ProgressData {
status: GenerationStatus;
progress: number;
message: string;
}
interface ProgressTrackerProps {
generationId: string;
repoUrl: string;
}
const STEPS: { status: GenerationStatus; label: string }[] = [
{ status: "QUEUED", label: "Queued" },
{ status: "CLONING", label: "Cloning Repository" },
{ status: "PARSING", label: "Analyzing Code" },
{ status: "GENERATING", label: "Generating Docs" },
{ status: "RENDERING", label: "Finalizing" },
];
export function ProgressTracker({
generationId,
repoUrl,
}: ProgressTrackerProps) {
const [data, setData] = useState<ProgressData>({
status: "QUEUED",
progress: 0,
message: "Waiting in queue...",
});
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const eventSource = new EventSource(`/api/status/${generationId}`);
eventSource.addEventListener("progress", (event) => {
try {
const parsed = JSON.parse(event.data);
setData(parsed);
if (parsed.status === "COMPLETED" || parsed.status === "FAILED") {
eventSource.close();
}
} catch {
setError("Failed to parse progress data");
}
});
eventSource.addEventListener("timeout", () => {
setError("Connection timed out. Please refresh the page.");
eventSource.close();
});
eventSource.onerror = () => {
setError("Connection error. Please refresh the page.");
eventSource.close();
};
return () => {
eventSource.close();
};
}, [generationId]);
const getStepIndex = (status: GenerationStatus) => {
if (status === "COMPLETED") return STEPS.length;
if (status === "FAILED") return -1;
return STEPS.findIndex((s) => s.status === status);
};
const currentStepIndex = getStepIndex(data.status);
const isCompleted = data.status === "COMPLETED";
const isFailed = data.status === "FAILED";
return (
<div className="space-y-6">
<div className="relative h-2 bg-zinc-800 rounded-full overflow-hidden">
<div
className="absolute inset-y-0 left-0 bg-gradient-to-r from-blue-500 via-indigo-500 to-purple-500 transition-all duration-500 ease-out"
style={{ width: `${data.progress}%` }}
/>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-zinc-400">{data.message}</span>
<span className="text-zinc-500 font-mono">{data.progress}%</span>
</div>
<div className="space-y-3">
{STEPS.map((step, index) => {
const isActive = index === currentStepIndex;
const isDone = index < currentStepIndex || isCompleted;
return (
<div
key={step.status}
className={`flex items-center gap-4 p-4 rounded-xl border transition-all duration-300 ${
isActive
? "bg-blue-500/10 border-blue-500/30"
: isDone
? "bg-zinc-900/50 border-zinc-800"
: "bg-transparent border-zinc-800/50"
}`}
>
<div
className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center ${
isActive
? "bg-blue-500/20 text-blue-400"
: isDone
? "bg-green-500/20 text-green-400"
: "bg-zinc-800 text-zinc-500"
}`}
>
{isActive ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : isDone ? (
<CheckCircle2 className="w-4 h-4" />
) : (
<Circle className="w-4 h-4" />
)}
</div>
<div
className={`flex-1 ${
isActive
? "text-white"
: isDone
? "text-green-400"
: "text-zinc-500"
}`}
>
{step.label}
</div>
</div>
{isActive && (
<div className="flex-shrink-0">
<div className="w-2 h-2 rounded-full bg-blue-400 animate-pulse" />
</div>
)}
</div>
))}
</div>
{isFailed && (
<div className="p-4 rounded-xl bg-red-500/10 border border-red-500/20 text-red-400 text-sm">
<div className="flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" />
<div>
<p className="text-red-400 font-medium">Generation Failed</p>
<p className="text-red-400/70 text-sm mt-1">
Something went wrong. Please try again.
</p>
</div>
</div>
<button
onClick={() => window.location.reload()}
className="mt-4 flex items-center gap-2 px-4 py-2 bg-red-500/20 hover:bg-red-500/30 text-red-400 rounded-lg transition-colors text-sm"
>
<RefreshCw className="w-4 h-4" />
Try Again
</button>
</div>
)}
{isCompleted && (
<div className="p-6 rounded-xl bg-green-500/10 border border-green-500/20 text-center">
<div className="w-12 h-12 rounded-full bg-green-500/20 flex items-center justify-center mx-auto mb-4">
<CheckCircle2 className="w-6 h-6 text-green-400" />
</div>
<h3 className="text-lg font-semibold text-white mb-2">
Documentation Ready!
</h3>
<p className="text-zinc-400 text-sm mb-6">
Your interactive documentation has been generated successfully.
</p>
<Link
href={`/docs/${generationId}`}
className="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 text-white font-medium rounded-xl transition-all"
>
<FileText className="w-4 h-4" />
View Documentation
</Link>
</div>
)}
{error && !isFailed && (
<div className="p-4 rounded-xl bg-red-500/10 border border-red-500/20 text-red-400 text-sm">
<div className="flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" />
<div>
<p className="text-red-400 font-medium">Connection Error</p>
<p className="text-red-400/70 text-sm mt-1">
{error}
</p>
</div>
</div>
)}
</div>
);
}