refactor: simplify overview page — header above chat, remove widget grid

Move project name/badges/Refresh/Open IDE above the agent chat panel.
Remove stats, code repo, deployment, PRs, issues, resources sections.

Made-with: Cursor
This commit is contained in:
2026-03-01 16:01:35 -08:00
parent 26a11412b5
commit 296324f424

View File

@@ -3,126 +3,33 @@
import { useEffect, useState } from "react";
import { useParams } from "next/navigation";
import { useSession } from "next-auth/react";
import {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { OrchestratorChat } from "@/components/OrchestratorChat";
import { AtlasChat } from "@/components/AtlasChat";
import {
GitBranch,
GitCommit,
GitPullRequest,
CircleDot,
ExternalLink,
Terminal,
Rocket,
Database,
Loader2,
CheckCircle2,
XCircle,
Clock,
AlertCircle,
Code2,
RefreshCw,
} from "lucide-react";
import Link from "next/link";
import { toast } from "sonner";
interface ContextSnapshot {
lastCommit?: {
sha: string;
message: string;
author?: string;
timestamp?: string;
url?: string;
};
currentBranch?: string;
recentCommits?: { sha: string; message: string; author?: string; timestamp?: string }[];
openPRs?: { number: number; title: string; url: string; from: string; into: string }[];
openIssues?: { number: number; title: string; url: string; labels?: string[] }[];
lastDeployment?: {
status: string;
url?: string;
timestamp?: string;
deploymentUuid?: string;
};
updatedAt?: string;
}
interface Project {
id: string;
name: string;
productName: string;
productVision?: string;
slug?: string;
workspace?: string;
status?: string;
currentPhase?: string;
projectType?: string;
// Gitea
giteaRepo?: string;
giteaRepoUrl?: string;
giteaCloneUrl?: string;
giteaSshUrl?: string;
giteaWebhookId?: number;
giteaError?: string;
// Coolify
coolifyProjectUuid?: string;
coolifyAppUuid?: string;
coolifyDbUuid?: string;
deploymentUrl?: string;
// Theia
theiaWorkspaceUrl?: string;
// Stage
stage?: 'discovery' | 'architecture' | 'building' | 'active';
prd?: string;
// Context
contextSnapshot?: ContextSnapshot;
stats?: { sessions: number; costs: number };
createdAt?: string;
updatedAt?: string;
}
function timeAgo(ts?: string): string {
if (!ts) return "—";
const d = new Date(ts);
if (isNaN(d.getTime())) return "—";
const diff = (Date.now() - d.getTime()) / 1000;
if (diff < 60) return "just now";
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
return `${Math.floor(diff / 86400)}d ago`;
}
function DeployBadge({ status }: { status?: string }) {
if (!status) return <Badge variant="secondary">No deployments</Badge>;
const map: Record<string, { label: string; icon: React.ElementType; className: string }> = {
finished: { label: "Deployed", icon: CheckCircle2, className: "bg-green-500/10 text-green-600 border-green-500/20" },
in_progress: { label: "Deploying", icon: Loader2, className: "bg-blue-500/10 text-blue-600 border-blue-500/20" },
queued: { label: "Queued", icon: Clock, className: "bg-yellow-500/10 text-yellow-600 border-yellow-500/20" },
failed: { label: "Failed", icon: XCircle, className: "bg-red-500/10 text-red-600 border-red-500/20" },
cancelled: { label: "Cancelled", icon: XCircle, className: "bg-gray-500/10 text-gray-500 border-gray-500/20" },
};
const cfg = map[status] ?? { label: status, icon: AlertCircle, className: "bg-gray-500/10 text-gray-500" };
const Icon = cfg.icon;
return (
<span className={`inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium border ${cfg.className}`}>
<Icon className="h-3 w-3" />
{cfg.label}
</span>
);
}
export default function ProjectOverviewPage() {
const params = useParams();
const projectId = params.projectId as string;
const workspace = params.workspace as string;
const { status: authStatus } = useSession();
const [project, setProject] = useState<Project | null>(null);
@@ -188,34 +95,16 @@ export default function ProjectOverviewPage() {
if (error || !project) {
return (
<div className="container mx-auto py-8 px-6 max-w-5xl">
<Card className="border-red-500/30 bg-red-500/5">
<CardContent className="py-8 text-center">
<p className="text-sm text-red-600">{error ?? "Project not found"}</p>
</CardContent>
</Card>
<div className="rounded-xl border border-red-500/30 bg-red-500/5 py-8 text-center">
<p className="text-sm text-red-600">{error ?? "Project not found"}</p>
</div>
</div>
);
}
const snap = project.contextSnapshot;
const gitea_url = process.env.NEXT_PUBLIC_GITEA_URL ?? "https://git.vibnai.com";
return (
<div className="container mx-auto py-8 px-6 max-w-5xl space-y-6">
{/* ── Agent Panel — Atlas for discovery, Orchestrator once PRD is done ── */}
{(!project.stage || project.stage === 'discovery') ? (
<AtlasChat
projectId={projectId}
projectName={project.productName}
/>
) : (
<OrchestratorChat
projectId={projectId}
projectName={project.productName}
/>
)}
{/* ── Header ── */}
<div className="flex items-start justify-between">
<div>
@@ -255,268 +144,17 @@ export default function ProjectOverviewPage() {
</div>
</div>
{/* ── Quick Stats ── */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{[
{ label: "Sessions", value: project.stats?.sessions ?? 0 },
{ label: "AI Cost", value: `$${(project.stats?.costs ?? 0).toFixed(2)}` },
{ label: "Open PRs", value: snap?.openPRs?.length ?? 0 },
{ label: "Open Issues", value: snap?.openIssues?.length ?? 0 },
].map(({ label, value }) => (
<Card key={label}>
<CardContent className="pt-5 pb-4">
<p className="text-2xl font-bold">{value}</p>
<p className="text-xs text-muted-foreground mt-0.5">{label}</p>
</CardContent>
</Card>
))}
</div>
<div className="grid md:grid-cols-2 gap-6">
{/* ── Code / Gitea ── */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-semibold flex items-center gap-2">
<Code2 className="h-4 w-4" />
Code Repository
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{project.giteaRepo ? (
<>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-sm font-mono text-muted-foreground">
<GitBranch className="h-3.5 w-3.5" />
{snap?.currentBranch ?? "main"}
</div>
<a
href={project.giteaRepoUrl ?? `${gitea_url}/${project.giteaRepo}`}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-primary flex items-center gap-1 hover:underline"
>
{project.giteaRepo}
<ExternalLink className="h-3 w-3" />
</a>
</div>
{snap?.lastCommit ? (
<div className="rounded-md border bg-muted/30 p-3 space-y-1">
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<GitCommit className="h-3.5 w-3.5" />
<span className="font-mono">{snap.lastCommit.sha.slice(0, 8)}</span>
<span>·</span>
<span>{timeAgo(snap.lastCommit.timestamp)}</span>
{snap.lastCommit.author && <span>· {snap.lastCommit.author}</span>}
</div>
<p className="text-sm font-medium line-clamp-1">{snap.lastCommit.message}</p>
</div>
) : (
<p className="text-xs text-muted-foreground">No commits yet push to get started</p>
)}
<div className="text-xs text-muted-foreground space-y-1 pt-1 border-t">
<p className="font-medium text-foreground">Clone</p>
<p className="font-mono break-all">{project.giteaCloneUrl}</p>
{project.giteaSshUrl && (
<p className="font-mono break-all">{project.giteaSshUrl}</p>
)}
</div>
</>
) : (
<div className="text-center py-4">
<p className="text-sm text-muted-foreground">
{project.giteaError
? `Repo provisioning failed: ${project.giteaError}`
: "No repository linked"}
</p>
</div>
)}
</CardContent>
</Card>
{/* ── Deployment ── */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-semibold flex items-center gap-2">
<Rocket className="h-4 w-4" />
Deployment
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{snap?.lastDeployment ? (
<>
<div className="flex items-center justify-between">
<DeployBadge status={snap.lastDeployment.status} />
<span className="text-xs text-muted-foreground">{timeAgo(snap.lastDeployment.timestamp)}</span>
</div>
{snap.lastDeployment.url && (
<a
href={snap.lastDeployment.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 text-sm text-primary hover:underline"
>
<ExternalLink className="h-3.5 w-3.5" />
{snap.lastDeployment.url}
</a>
)}
</>
) : (
<div className="text-center py-4 space-y-3">
<p className="text-sm text-muted-foreground">No deployments yet</p>
<Button size="sm" variant="outline" asChild>
<Link href={`/${workspace}/project/${projectId}/deployment`}>
Set up deployment
</Link>
</Button>
</div>
)}
</CardContent>
</Card>
{/* ── Open PRs ── */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-semibold flex items-center gap-2">
<GitPullRequest className="h-4 w-4" />
Pull Requests
{(snap?.openPRs?.length ?? 0) > 0 && (
<Badge variant="secondary" className="ml-auto">{snap!.openPRs!.length} open</Badge>
)}
</CardTitle>
</CardHeader>
<CardContent>
{snap?.openPRs?.length ? (
<ul className="space-y-2">
{snap.openPRs.map(pr => (
<li key={pr.number}>
<a
href={pr.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-start gap-2 text-sm hover:bg-accent rounded-md p-2 -mx-2 transition-colors"
>
<span className="text-muted-foreground font-mono text-xs mt-0.5">#{pr.number}</span>
<div className="flex-1 min-w-0">
<p className="font-medium line-clamp-1">{pr.title}</p>
<p className="text-xs text-muted-foreground">{pr.from} {pr.into}</p>
</div>
<ExternalLink className="h-3.5 w-3.5 text-muted-foreground shrink-0 mt-0.5" />
</a>
</li>
))}
</ul>
) : (
<p className="text-sm text-muted-foreground text-center py-4">No open pull requests</p>
)}
</CardContent>
</Card>
{/* ── Open Issues ── */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-semibold flex items-center gap-2">
<CircleDot className="h-4 w-4" />
Issues
{(snap?.openIssues?.length ?? 0) > 0 && (
<Badge variant="secondary" className="ml-auto">{snap!.openIssues!.length} open</Badge>
)}
</CardTitle>
</CardHeader>
<CardContent>
{snap?.openIssues?.length ? (
<ul className="space-y-2">
{snap.openIssues.map(issue => (
<li key={issue.number}>
<a
href={issue.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-start gap-2 text-sm hover:bg-accent rounded-md p-2 -mx-2 transition-colors"
>
<span className="text-muted-foreground font-mono text-xs mt-0.5">#{issue.number}</span>
<div className="flex-1 min-w-0">
<p className="font-medium line-clamp-1">{issue.title}</p>
{issue.labels?.length ? (
<div className="flex gap-1 flex-wrap mt-0.5">
{issue.labels.map(l => (
<span key={l} className="text-[10px] px-1.5 py-0.5 bg-muted rounded-full">{l}</span>
))}
</div>
) : null}
</div>
<ExternalLink className="h-3.5 w-3.5 text-muted-foreground shrink-0 mt-0.5" />
</a>
</li>
))}
</ul>
) : (
<p className="text-sm text-muted-foreground text-center py-4">No open issues</p>
)}
</CardContent>
</Card>
</div>
{/* ── Recent Commits ── */}
{snap?.recentCommits && snap.recentCommits.length > 1 && (
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-semibold flex items-center gap-2">
<GitCommit className="h-4 w-4" />
Recent Commits
</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-2">
{snap.recentCommits.map((c, i) => (
<li key={i} className="flex items-center gap-3 text-sm py-1.5 border-b last:border-0">
<span className="font-mono text-xs text-muted-foreground w-16 shrink-0">{c.sha.slice(0, 8)}</span>
<span className="flex-1 line-clamp-1">{c.message}</span>
<span className="text-xs text-muted-foreground shrink-0">{c.author ?? ""}</span>
<span className="text-xs text-muted-foreground shrink-0 w-16 text-right">{timeAgo(c.timestamp)}</span>
</li>
))}
</ul>
</CardContent>
</Card>
)}
{/* ── Resources ── */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-semibold flex items-center gap-2">
<Database className="h-4 w-4" />
Resources
</CardTitle>
<CardDescription className="text-xs">Databases and services linked to this project</CardDescription>
</CardHeader>
<CardContent>
{project.coolifyDbUuid ? (
<div className="flex items-center gap-2 text-sm">
<CheckCircle2 className="h-4 w-4 text-green-500" />
<span>Database provisioned</span>
<Badge variant="outline" className="text-xs ml-auto">{project.coolifyDbUuid}</Badge>
</div>
) : (
<div className="flex items-center justify-between">
<p className="text-sm text-muted-foreground">No databases provisioned yet</p>
<Button size="sm" variant="outline">
<Database className="h-3.5 w-3.5 mr-1.5" />
Add Database
</Button>
</div>
)}
</CardContent>
</Card>
{/* ── Context snapshot freshness ── */}
{snap?.updatedAt && (
<p className="text-xs text-muted-foreground text-right">
Context updated {timeAgo(snap.updatedAt)} via webhooks
</p>
{/* ── Agent Panel — Atlas for discovery, Orchestrator once PRD is done ── */}
{(!project.stage || project.stage === 'discovery') ? (
<AtlasChat
projectId={projectId}
projectName={project.productName}
/>
) : (
<OrchestratorChat
projectId={projectId}
projectName={project.productName}
/>
)}
</div>