New /history page shows all past generations for a repo and allows selecting two to compare side-by-side. Displays tech stack diffs, architecture diagrams, key metrics changes, and module breakdowns. Added Version History link to doc viewer header.
792 lines
27 KiB
TypeScript
792 lines
27 KiB
TypeScript
"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 (
|
|
<div className="flex flex-wrap gap-2">
|
|
{removed.map((tech) => (
|
|
<span
|
|
key={`removed-${tech}`}
|
|
className="px-3 py-1 text-sm bg-red-500/10 border border-red-500/30 rounded-full text-red-300 line-through"
|
|
title="Removed"
|
|
>
|
|
{tech}
|
|
</span>
|
|
))}
|
|
{unchanged.map((tech) => (
|
|
<span
|
|
key={tech}
|
|
className="px-3 py-1 text-sm bg-white/5 border border-white/10 rounded-full text-zinc-400"
|
|
>
|
|
{tech}
|
|
</span>
|
|
))}
|
|
{added.map((tech) => (
|
|
<span
|
|
key={`added-${tech}`}
|
|
className="px-3 py-1 text-sm bg-green-500/10 border border-green-500/30 rounded-full text-green-300"
|
|
title="Added"
|
|
>
|
|
{tech}
|
|
</span>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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 (
|
|
<div className="fixed inset-0 z-50 bg-[#0a0a0f] overflow-auto">
|
|
<div className="sticky top-0 z-10 border-b border-white/10 bg-black/50 backdrop-blur-sm">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-4">
|
|
<GitCompare className="w-6 h-6 text-blue-400" />
|
|
<h2 className="text-xl font-bold text-white">Version Comparison</h2>
|
|
</div>
|
|
<button
|
|
onClick={onClose}
|
|
className="p-2 rounded-lg text-zinc-400 hover:text-white hover:bg-white/5 transition-colors"
|
|
>
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
{/* Left Panel - Older */}
|
|
<div className="space-y-6">
|
|
<div className="glass rounded-xl p-4 border-l-4 border-l-zinc-500">
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<GitCommit className="w-4 h-4 text-zinc-400" />
|
|
<code className="text-sm text-zinc-300">
|
|
{leftGen.commitHash.slice(0, 7)}
|
|
</code>
|
|
<span className="text-xs text-zinc-500">•</span>
|
|
<span className="text-sm text-zinc-400">
|
|
{formatDate(leftGen.createdAt)}
|
|
</span>
|
|
</div>
|
|
<p className="text-xs text-zinc-500">Older version</p>
|
|
</div>
|
|
|
|
<div className="glass rounded-xl p-6">
|
|
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
|
<BookOpen className="w-5 h-5 text-blue-400" />
|
|
Overview
|
|
</h3>
|
|
<p className="text-zinc-300 text-sm leading-relaxed mb-4">
|
|
{leftOverview.description}
|
|
</p>
|
|
|
|
<h4 className="text-sm font-medium text-zinc-400 mb-3">
|
|
Tech Stack
|
|
</h4>
|
|
<div className="flex flex-wrap gap-2">
|
|
{leftOverview.techStack.map((tech) => (
|
|
<span
|
|
key={tech}
|
|
className="px-3 py-1 text-sm bg-white/5 border border-white/10 rounded-full text-zinc-300"
|
|
>
|
|
{tech}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="glass rounded-xl p-6">
|
|
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
|
<Layers className="w-5 h-5 text-blue-400" />
|
|
Architecture
|
|
</h3>
|
|
<MermaidDiagram chart={leftOverview.architectureDiagram} />
|
|
</div>
|
|
|
|
<div className="grid grid-cols-3 gap-3">
|
|
<div className="p-4 glass rounded-lg text-center">
|
|
<div className="text-2xl font-bold text-white">
|
|
{leftOverview.keyMetrics.files}
|
|
</div>
|
|
<div className="text-sm text-zinc-500">Files</div>
|
|
</div>
|
|
<div className="p-4 glass rounded-lg text-center">
|
|
<div className="text-2xl font-bold text-white">
|
|
{leftOverview.keyMetrics.modules}
|
|
</div>
|
|
<div className="text-sm text-zinc-500">Modules</div>
|
|
</div>
|
|
<div className="p-4 glass rounded-lg text-center">
|
|
<div className="text-2xl font-bold text-white">
|
|
{leftOverview.keyMetrics.languages.length}
|
|
</div>
|
|
<div className="text-sm text-zinc-500">Languages</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right Panel - Newer */}
|
|
<div className="space-y-6">
|
|
<div className="glass rounded-xl p-4 border-l-4 border-l-green-500">
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<GitCommit className="w-4 h-4 text-zinc-400" />
|
|
<code className="text-sm text-zinc-300">
|
|
{rightGen.commitHash.slice(0, 7)}
|
|
</code>
|
|
<span className="text-xs text-zinc-500">•</span>
|
|
<span className="text-sm text-zinc-400">
|
|
{formatDate(rightGen.createdAt)}
|
|
</span>
|
|
</div>
|
|
<p className="text-xs text-green-400">Newer version</p>
|
|
</div>
|
|
|
|
<div className="glass rounded-xl p-6">
|
|
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
|
<BookOpen className="w-5 h-5 text-blue-400" />
|
|
Overview
|
|
</h3>
|
|
<p className="text-zinc-300 text-sm leading-relaxed mb-4">
|
|
{rightOverview.description}
|
|
</p>
|
|
|
|
<h4 className="text-sm font-medium text-zinc-400 mb-3">
|
|
Tech Stack Changes
|
|
</h4>
|
|
<TechStackDiff
|
|
leftStack={leftOverview.techStack}
|
|
rightStack={rightOverview.techStack}
|
|
/>
|
|
</div>
|
|
|
|
<div className="glass rounded-xl p-6">
|
|
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
|
<Layers className="w-5 h-5 text-blue-400" />
|
|
Architecture
|
|
</h3>
|
|
<MermaidDiagram chart={rightOverview.architectureDiagram} />
|
|
</div>
|
|
|
|
<div className="grid grid-cols-3 gap-3">
|
|
<div className="p-4 glass rounded-lg text-center relative overflow-hidden">
|
|
<div
|
|
className={`text-2xl font-bold ${
|
|
filesDiff > 0
|
|
? "text-green-400"
|
|
: filesDiff < 0
|
|
? "text-red-400"
|
|
: "text-white"
|
|
}`}
|
|
>
|
|
{rightOverview.keyMetrics.files}
|
|
</div>
|
|
<div className="text-sm text-zinc-500">Files</div>
|
|
{filesDiff !== 0 && (
|
|
<div
|
|
className={`absolute top-1 right-2 text-xs ${
|
|
filesDiff > 0 ? "text-green-400" : "text-red-400"
|
|
}`}
|
|
>
|
|
{filesDiff > 0 ? "+" : ""}
|
|
{filesDiff}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="p-4 glass rounded-lg text-center relative overflow-hidden">
|
|
<div
|
|
className={`text-2xl font-bold ${
|
|
modulesDiff > 0
|
|
? "text-green-400"
|
|
: modulesDiff < 0
|
|
? "text-red-400"
|
|
: "text-white"
|
|
}`}
|
|
>
|
|
{rightOverview.keyMetrics.modules}
|
|
</div>
|
|
<div className="text-sm text-zinc-500">Modules</div>
|
|
{modulesDiff !== 0 && (
|
|
<div
|
|
className={`absolute top-1 right-2 text-xs ${
|
|
modulesDiff > 0 ? "text-green-400" : "text-red-400"
|
|
}`}
|
|
>
|
|
{modulesDiff > 0 ? "+" : ""}
|
|
{modulesDiff}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="p-4 glass rounded-lg text-center relative overflow-hidden">
|
|
<div
|
|
className={`text-2xl font-bold ${
|
|
languagesDiff > 0
|
|
? "text-green-400"
|
|
: languagesDiff < 0
|
|
? "text-red-400"
|
|
: "text-white"
|
|
}`}
|
|
>
|
|
{rightOverview.keyMetrics.languages.length}
|
|
</div>
|
|
<div className="text-sm text-zinc-500">Languages</div>
|
|
{languagesDiff !== 0 && (
|
|
<div
|
|
className={`absolute top-1 right-2 text-xs ${
|
|
languagesDiff > 0 ? "text-green-400" : "text-red-400"
|
|
}`}
|
|
>
|
|
{languagesDiff > 0 ? "+" : ""}
|
|
{languagesDiff}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Module Comparison */}
|
|
<div className="mt-8">
|
|
<h3 className="text-2xl font-bold text-white mb-6 flex items-center gap-3">
|
|
<Folder className="w-6 h-6 text-blue-400" />
|
|
Module Breakdown Comparison
|
|
</h3>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<div className="glass rounded-xl p-6">
|
|
<h4 className="font-semibold text-white mb-4 text-zinc-400">
|
|
Older Version ({left.sections.modules.length} modules)
|
|
</h4>
|
|
<div className="space-y-2 max-h-96 overflow-auto">
|
|
{left.sections.modules.map((module) => (
|
|
<div
|
|
key={module.name}
|
|
className="flex items-center gap-3 p-3 rounded-lg bg-white/5"
|
|
>
|
|
<FileCode className="w-4 h-4 text-zinc-500" />
|
|
<div className="flex-1 min-w-0">
|
|
<div className="text-sm text-zinc-300 truncate">
|
|
{module.name}
|
|
</div>
|
|
<div className="text-xs text-zinc-500 truncate">
|
|
{module.path}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="glass rounded-xl p-6">
|
|
<h4 className="font-semibold text-white mb-4 text-green-400">
|
|
Newer Version ({right.sections.modules.length} modules)
|
|
</h4>
|
|
<div className="space-y-2 max-h-96 overflow-auto">
|
|
{right.sections.modules.map((module) => {
|
|
const existedInLeft = left.sections.modules.some(
|
|
(m) => m.name === module.name
|
|
);
|
|
return (
|
|
<div
|
|
key={module.name}
|
|
className={`flex items-center gap-3 p-3 rounded-lg ${
|
|
existedInLeft ? "bg-white/5" : "bg-green-500/10 border border-green-500/20"
|
|
}`}
|
|
>
|
|
<FileCode
|
|
className={`w-4 h-4 ${
|
|
existedInLeft ? "text-zinc-500" : "text-green-400"
|
|
}`}
|
|
/>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="text-sm text-zinc-300 truncate">
|
|
{module.name}
|
|
{!existedInLeft && (
|
|
<span className="ml-2 text-xs text-green-400">
|
|
(new)
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="text-xs text-zinc-500 truncate">
|
|
{module.path}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function HistoryContent() {
|
|
const searchParams = useSearchParams();
|
|
const repo = searchParams.get("repo");
|
|
|
|
const [generations, setGenerations] = useState<Generation[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
|
const [comparing, setComparing] = useState(false);
|
|
const [leftDoc, setLeftDoc] = useState<GeneratedDocs | null>(null);
|
|
const [rightDoc, setRightDoc] = useState<GeneratedDocs | null>(null);
|
|
const [leftGen, setLeftGen] = useState<Generation | null>(null);
|
|
const [rightGen, setRightGen] = useState<Generation | null>(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 (
|
|
<div className="min-h-screen flex items-center justify-center">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-6 h-6 border-2 border-blue-500/30 border-t-blue-500 rounded-full animate-spin" />
|
|
<span className="text-zinc-400">Loading history...</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center">
|
|
<div className="glass rounded-xl p-8 text-center max-w-md">
|
|
<div className="text-red-400 mb-2">Error</div>
|
|
<p className="text-zinc-400">{error}</p>
|
|
<Link
|
|
href="/"
|
|
className="inline-flex items-center gap-2 mt-4 text-blue-400 hover:text-blue-300"
|
|
>
|
|
<ArrowLeft className="w-4 h-4" />
|
|
Back to Home
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!repo) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center">
|
|
<div className="glass rounded-xl p-8 text-center max-w-md">
|
|
<History className="w-12 h-12 text-zinc-500 mx-auto mb-4" />
|
|
<h2 className="text-xl font-bold text-white mb-2">
|
|
No Repository Specified
|
|
</h2>
|
|
<p className="text-zinc-400 mb-4">
|
|
Please provide a repository URL to view its history.
|
|
</p>
|
|
<Link
|
|
href="/"
|
|
className="inline-flex items-center gap-2 btn-primary"
|
|
>
|
|
<ArrowLeft className="w-4 h-4" />
|
|
Back to Home
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (generations.length === 0) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center">
|
|
<div className="glass rounded-xl p-8 text-center max-w-md">
|
|
<History className="w-12 h-12 text-zinc-500 mx-auto mb-4" />
|
|
<h2 className="text-xl font-bold text-white mb-2">No History Found</h2>
|
|
<p className="text-zinc-400 mb-4">
|
|
No documentation has been generated for this repository yet.
|
|
</p>
|
|
<Link
|
|
href="/"
|
|
className="inline-flex items-center gap-2 btn-primary"
|
|
>
|
|
<ArrowLeft className="w-4 h-4" />
|
|
Back to Home
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (generations.length === 1) {
|
|
return (
|
|
<div className="min-h-screen">
|
|
<div className="border-b border-white/10 bg-black/20">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
|
<div className="flex items-center justify-between">
|
|
<Link
|
|
href="/"
|
|
className="flex items-center gap-2 text-sm text-zinc-400 hover:text-white transition-colors"
|
|
>
|
|
<ArrowLeft className="w-4 h-4" />
|
|
Back to Home
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
|
<div className="text-center mb-12">
|
|
<History className="w-12 h-12 text-zinc-500 mx-auto mb-4" />
|
|
<h1 className="text-3xl font-bold text-white mb-2">
|
|
Version History
|
|
</h1>
|
|
<p className="text-zinc-400">
|
|
{generations[0].repoName}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="glass rounded-xl p-8 text-center">
|
|
<div className="w-16 h-16 rounded-full bg-blue-500/10 border border-blue-500/20 flex items-center justify-center mx-auto mb-4">
|
|
<GitCommit className="w-8 h-8 text-blue-400" />
|
|
</div>
|
|
<h2 className="text-xl font-bold text-white mb-2">
|
|
Only One Version Exists
|
|
</h2>
|
|
<p className="text-zinc-400 max-w-md mx-auto">
|
|
Generate docs again after code changes to compare versions and track
|
|
how your architecture evolves over time.
|
|
</p>
|
|
|
|
<div className="mt-8 p-4 rounded-lg bg-white/5 max-w-md mx-auto text-left">
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<GitCommit className="w-4 h-4 text-zinc-400" />
|
|
<code className="text-sm text-zinc-300">
|
|
{generations[0].commitHash.slice(0, 7)}
|
|
</code>
|
|
</div>
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<Clock className="w-4 h-4 text-zinc-400" />
|
|
<span className="text-sm text-zinc-300">
|
|
{formatDate(generations[0].createdAt)}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<Clock className="w-4 h-4 text-zinc-400" />
|
|
<span className="text-sm text-zinc-300">
|
|
Generated in {formatDuration(generations[0].duration)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<Link
|
|
href={`/docs/${generations[0].id}`}
|
|
className="inline-flex items-center gap-2 btn-primary mt-8"
|
|
>
|
|
<BookOpen className="w-4 h-4" />
|
|
View Documentation
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen">
|
|
<div className="border-b border-white/10 bg-black/20">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
|
<div className="flex items-center justify-between">
|
|
<Link
|
|
href="/"
|
|
className="flex items-center gap-2 text-sm text-zinc-400 hover:text-white transition-colors"
|
|
>
|
|
<ArrowLeft className="w-4 h-4" />
|
|
Back to Home
|
|
</Link>
|
|
<span className="text-sm text-zinc-500">
|
|
{selectedIds.size} of 2 selected
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
|
<div className="text-center mb-12">
|
|
<History className="w-12 h-12 text-zinc-500 mx-auto mb-4" />
|
|
<h1 className="text-3xl font-bold text-white mb-2">Version History</h1>
|
|
<p className="text-zinc-400">{generations[0]?.repoName}</p>
|
|
<p className="text-sm text-zinc-500 mt-2">
|
|
Select any 2 versions to compare side-by-side
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
{generations.map((gen) => {
|
|
const isSelected = selectedIds.has(gen.id);
|
|
const canSelect = selectedIds.size < 2 || isSelected;
|
|
|
|
return (
|
|
<div
|
|
key={gen.id}
|
|
className={`glass rounded-xl p-4 transition-all ${
|
|
isSelected
|
|
? "border-blue-500/50 bg-blue-500/5"
|
|
: "border-white/10"
|
|
} ${!canSelect ? "opacity-50" : ""}`}
|
|
>
|
|
<div className="flex items-center gap-4">
|
|
<button
|
|
onClick={() => canSelect && toggleSelection(gen.id)}
|
|
className={`flex-shrink-0 p-2 rounded-lg transition-colors ${
|
|
isSelected
|
|
? "text-blue-400 hover:bg-blue-500/10"
|
|
: "text-zinc-500 hover:text-zinc-300 hover:bg-white/5"
|
|
} ${!canSelect ? "cursor-not-allowed" : ""}`}
|
|
disabled={!canSelect}
|
|
>
|
|
{isSelected ? (
|
|
<CheckSquare className="w-5 h-5" />
|
|
) : (
|
|
<Square className="w-5 h-5" />
|
|
)}
|
|
</button>
|
|
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-3 flex-wrap">
|
|
<GitCommit className="w-4 h-4 text-zinc-500" />
|
|
<code className="text-sm text-zinc-300">
|
|
{gen.commitHash.slice(0, 7)}
|
|
</code>
|
|
<span className="text-zinc-600">•</span>
|
|
<span className="text-sm text-zinc-400">
|
|
{formatDate(gen.createdAt)}
|
|
</span>
|
|
<span className="text-zinc-600">•</span>
|
|
<span className="text-sm text-zinc-500">
|
|
{formatDuration(gen.duration)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<Link
|
|
href={`/docs/${gen.id}`}
|
|
className="flex-shrink-0 px-3 py-1.5 text-sm text-zinc-400 hover:text-white bg-white/5 hover:bg-white/10 rounded-lg transition-colors"
|
|
>
|
|
View
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{selectedIds.size === 2 && (
|
|
<div className="mt-8 flex justify-center animate-slide-up">
|
|
<button
|
|
onClick={handleCompare}
|
|
disabled={comparing}
|
|
className="inline-flex items-center gap-2 btn-primary"
|
|
>
|
|
{comparing ? (
|
|
<>
|
|
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
|
Loading...
|
|
</>
|
|
) : (
|
|
<>
|
|
<GitCompare className="w-4 h-4" />
|
|
Compare Selected Versions
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{leftDoc && rightDoc && leftGen && rightGen && (
|
|
<ComparisonView
|
|
left={leftDoc}
|
|
right={rightDoc}
|
|
leftGen={leftGen}
|
|
rightGen={rightGen}
|
|
onClose={closeComparison}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function HistoryPage() {
|
|
return (
|
|
<Suspense
|
|
fallback={
|
|
<div className="min-h-screen flex items-center justify-center">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-6 h-6 border-2 border-blue-500/30 border-t-blue-500 rounded-full animate-spin" />
|
|
<span className="text-zinc-400">Loading...</span>
|
|
</div>
|
|
</div>
|
|
}
|
|
>
|
|
<HistoryContent />
|
|
</Suspense>
|
|
);
|
|
}
|