diff --git a/apps/web/src/components/progress-tracker.tsx b/apps/web/src/components/progress-tracker.tsx new file mode 100644 index 0000000..12cbe72 --- /dev/null +++ b/apps/web/src/components/progress-tracker.tsx @@ -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({ + status: "QUEUED", + progress: 0, + message: "Waiting in queue...", + }); + const [error, setError] = useState(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 ( +
+
+
+
+ +
+ {data.message} + {data.progress}% +
+ +
+ {STEPS.map((step, index) => { + const isActive = index === currentStepIndex; + const isDone = index < currentStepIndex || isCompleted; + + return ( +
+
+ {isActive ? ( + + ) : isDone ? ( + + ) : ( + + )} +
+
+ {step.label} +
+
+ + {isActive && ( +
+
+
+ )} +
+ ))} +
+ + {isFailed && ( +
+
+ +
+

Generation Failed

+

+ Something went wrong. Please try again. +

+
+
+ +
+ )} + + {isCompleted && ( +
+
+ +
+

+ Documentation Ready! +

+

+ Your interactive documentation has been generated successfully. +

+ + + View Documentation + +
+ )} + + {error && !isFailed && ( +
+
+ +
+

Connection Error

+

+ {error} +

+
+
+ )} +
+ ); +}