"use client";
import { Suspense, useEffect, useState } from "react";
import { useSearchParams } from "next/navigation";
import Link from "next/link";
import type { GeneratedDocs } from "@codeboard/shared";
import { MermaidDiagram } from "@/components/mermaid-diagram";
import {
ArrowLeft,
Clock,
GitCommit,
History,
CheckSquare,
Square,
GitCompare,
X,
BookOpen,
Layers,
Folder,
FileCode,
} from "lucide-react";
interface Generation {
id: string;
repoUrl: string;
repoName: string;
commitHash: string;
createdAt: string;
duration: number | null;
}
function formatDate(dateStr: string): string {
const date = new Date(dateStr);
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
});
}
function formatDuration(seconds: number | null): string {
if (!seconds) return "Unknown";
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
if (mins > 0) {
return `${mins}m ${secs}s`;
}
return `${secs}s`;
}
function TechStackDiff({
leftStack,
rightStack,
}: {
leftStack: string[];
rightStack: string[];
}) {
const leftSet = new Set(leftStack.map((s) => s.toLowerCase()));
const rightSet = new Set(rightStack.map((s) => s.toLowerCase()));
const added = rightStack.filter((s) => !leftSet.has(s.toLowerCase()));
const removed = leftStack.filter((s) => !rightSet.has(s.toLowerCase()));
const unchanged = leftStack.filter((s) => rightSet.has(s.toLowerCase()));
return (
{removed.map((tech) => (
{tech}
))}
{unchanged.map((tech) => (
{tech}
))}
{added.map((tech) => (
{tech}
))}
);
}
function ComparisonView({
left,
right,
leftGen,
rightGen,
onClose,
}: {
left: GeneratedDocs;
right: GeneratedDocs;
leftGen: Generation;
rightGen: Generation;
onClose: () => void;
}) {
const leftOverview = left.sections.overview;
const rightOverview = right.sections.overview;
const filesDiff = rightOverview.keyMetrics.files - leftOverview.keyMetrics.files;
const modulesDiff =
rightOverview.keyMetrics.modules - leftOverview.keyMetrics.modules;
const languagesDiff =
rightOverview.keyMetrics.languages.length -
leftOverview.keyMetrics.languages.length;
return (
{/* Left Panel - Older */}
{leftGen.commitHash.slice(0, 7)}
•
{formatDate(leftGen.createdAt)}
Older version
Overview
{leftOverview.description}
Tech Stack
{leftOverview.techStack.map((tech) => (
{tech}
))}
Architecture
{leftOverview.keyMetrics.files}
Files
{leftOverview.keyMetrics.modules}
Modules
{leftOverview.keyMetrics.languages.length}
Languages
{/* Right Panel - Newer */}
{rightGen.commitHash.slice(0, 7)}
•
{formatDate(rightGen.createdAt)}
Newer version
Overview
{rightOverview.description}
Tech Stack Changes
Architecture
0
? "text-green-400"
: filesDiff < 0
? "text-red-400"
: "text-white"
}`}
>
{rightOverview.keyMetrics.files}
Files
{filesDiff !== 0 && (
0 ? "text-green-400" : "text-red-400"
}`}
>
{filesDiff > 0 ? "+" : ""}
{filesDiff}
)}
0
? "text-green-400"
: modulesDiff < 0
? "text-red-400"
: "text-white"
}`}
>
{rightOverview.keyMetrics.modules}
Modules
{modulesDiff !== 0 && (
0 ? "text-green-400" : "text-red-400"
}`}
>
{modulesDiff > 0 ? "+" : ""}
{modulesDiff}
)}
0
? "text-green-400"
: languagesDiff < 0
? "text-red-400"
: "text-white"
}`}
>
{rightOverview.keyMetrics.languages.length}
Languages
{languagesDiff !== 0 && (
0 ? "text-green-400" : "text-red-400"
}`}
>
{languagesDiff > 0 ? "+" : ""}
{languagesDiff}
)}
{/* Module Comparison */}
Module Breakdown Comparison
Older Version ({left.sections.modules.length} modules)
{left.sections.modules.map((module) => (
{module.name}
{module.path}
))}
Newer Version ({right.sections.modules.length} modules)
{right.sections.modules.map((module) => {
const existedInLeft = left.sections.modules.some(
(m) => m.name === module.name
);
return (
{module.name}
{!existedInLeft && (
(new)
)}
{module.path}
);
})}
);
}
function HistoryContent() {
const searchParams = useSearchParams();
const repo = searchParams.get("repo");
const [generations, setGenerations] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [selectedIds, setSelectedIds] = useState>(new Set());
const [comparing, setComparing] = useState(false);
const [leftDoc, setLeftDoc] = useState(null);
const [rightDoc, setRightDoc] = useState(null);
const [leftGen, setLeftGen] = useState(null);
const [rightGen, setRightGen] = useState(null);
useEffect(() => {
if (!repo) {
setLoading(false);
setError("No repository URL provided");
return;
}
fetch(`/api/history?repo=${encodeURIComponent(repo)}`)
.then((res) => {
if (!res.ok) throw new Error("Failed to fetch history");
return res.json();
})
.then((data: Generation[]) => {
setGenerations(data);
setLoading(false);
})
.catch((err) => {
setError(err.message);
setLoading(false);
});
}, [repo]);
const toggleSelection = (id: string) => {
const newSelected = new Set(selectedIds);
if (newSelected.has(id)) {
newSelected.delete(id);
} else if (newSelected.size < 2) {
newSelected.add(id);
}
setSelectedIds(newSelected);
};
const handleCompare = async () => {
if (selectedIds.size !== 2) return;
const ids = Array.from(selectedIds);
const gen1 = generations.find((g) => g.id === ids[0])!;
const gen2 = generations.find((g) => g.id === ids[1])!;
// Sort by date - older first
const [olderGen, newerGen] =
new Date(gen1.createdAt) < new Date(gen2.createdAt)
? [gen1, gen2]
: [gen2, gen1];
setComparing(true);
try {
const [olderDocRes, newerDocRes] = await Promise.all([
fetch(`/api/docs/${olderGen.id}`),
fetch(`/api/docs/${newerGen.id}`),
]);
if (!olderDocRes.ok || !newerDocRes.ok) {
throw new Error("Failed to fetch documentation");
}
const [olderDoc, newerDoc] = await Promise.all([
olderDocRes.json(),
newerDocRes.json(),
]);
setLeftDoc(olderDoc);
setRightDoc(newerDoc);
setLeftGen(olderGen);
setRightGen(newerGen);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to compare");
setComparing(false);
}
};
const closeComparison = () => {
setLeftDoc(null);
setRightDoc(null);
setLeftGen(null);
setRightGen(null);
setComparing(false);
};
if (loading) {
return (
);
}
if (error) {
return (
Error
{error}
Back to Home
);
}
if (!repo) {
return (
No Repository Specified
Please provide a repository URL to view its history.
Back to Home
);
}
if (generations.length === 0) {
return (
No History Found
No documentation has been generated for this repository yet.
Back to Home
);
}
if (generations.length === 1) {
return (
Version History
{generations[0].repoName}
Only One Version Exists
Generate docs again after code changes to compare versions and track
how your architecture evolves over time.
{generations[0].commitHash.slice(0, 7)}
{formatDate(generations[0].createdAt)}
Generated in {formatDuration(generations[0].duration)}
View Documentation
);
}
return (
Back to Home
{selectedIds.size} of 2 selected
Version History
{generations[0]?.repoName}
Select any 2 versions to compare side-by-side
{generations.map((gen) => {
const isSelected = selectedIds.has(gen.id);
const canSelect = selectedIds.size < 2 || isSelected;
return (
{gen.commitHash.slice(0, 7)}
•
{formatDate(gen.createdAt)}
•
{formatDuration(gen.duration)}
View
);
})}
{selectedIds.size === 2 && (
)}
{leftDoc && rightDoc && leftGen && rightGen && (
)}
);
}
export default function HistoryPage() {
return (
}
>
);
}