feat: add progress tracker component
This commit is contained in:
211
apps/web/src/components/progress-tracker.tsx
Normal file
211
apps/web/src/components/progress-tracker.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user