"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@kandev/ui/card";
import { Button } from "@kandev/ui/button";
import { PageTopbar } from "@/components/page-topbar";
import { ToggleGroup, ToggleGroupItem } from "@kandev/ui/toggle-group";
import type { StatsResponse } from "@/lib/types/http";
import { useMemo, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { IconChartBar } from "@tabler/icons-react";
import {
OverviewCards,
WorkloadSection,
RepositoryStatsGrid,
TopRepositories,
RepoLeaders,
} from "./stats-sections";
import {
ActivityHeatmap,
AgentUsageList,
CompletedTasksChart,
MostProductiveSummary,
} from "./stats-charts";
import { PRStatsPanel } from "@/components/github/pr-stats";
interface StatsPageClientProps {
stats: StatsResponse | null;
error: string | null;
workspaceId?: string;
activeRange?: RangeKey;
}
const EMPTY_STATS: StatsResponse = {
global: {
total_tasks: 0,
completed_tasks: 0,
in_progress_tasks: 0,
total_sessions: 0,
total_turns: 0,
total_messages: 0,
total_user_messages: 0,
total_tool_calls: 0,
total_duration_ms: 0,
avg_turns_per_task: 0,
avg_messages_per_task: 0,
avg_duration_ms_per_task: 0,
},
task_stats: [],
daily_activity: [],
completed_activity: [],
agent_usage: [],
repository_stats: [],
git_stats: { total_commits: 0, total_files_changed: 0, total_insertions: 0, total_deletions: 0 },
};
function formatDuration(ms: number): string {
if (ms === 0) return "—";
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
if (hours > 0) return `${hours}h ${minutes % 60}m`;
if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
return `${seconds}s`;
}
type RangeKey = "week" | "month" | "all";
function getRangeLabel(range: RangeKey): string {
switch (range) {
case "week":
return "Last Week";
case "month":
return "Last Month";
case "all":
return "All Time";
default:
return "Last Month";
}
}
function StatsEmptyState({ message }: { message: string }) {
return (
);
}
type StatsHeaderProps = {
global: StatsResponse["global"];
range: RangeKey;
copied: boolean;
onRangeChange: (r: RangeKey) => void;
onCopy: () => void;
};
function StatsHeader({ global, range, copied, onRangeChange, onCopy }: StatsHeaderProps) {
return (
}
subtitle={`${global.total_tasks} tasks · ${global.total_sessions} sessions · ${formatDuration(global.total_duration_ms)}`}
actions={
<>
{
if (v) onRangeChange(v as RangeKey);
}}
variant="outline"
className="h-7"
>
{(["week", "month", "all"] as RangeKey[]).map((key) => (
{getRangeLabel(key)}
))}
>
}
/>
);
}
type StatsContentProps = { resolvedStats: StatsResponse; rangeLabel: string; workspaceId?: string };
function SectionDivider({ id, label }: { id: string; label: string }) {
return (
);
}
function TelemetrySection({
completedActivity,
dailyActivity,
agentUsage,
rangeLabel,
}: {
completedActivity: StatsResponse["completed_activity"];
dailyActivity: StatsResponse["daily_activity"];
agentUsage: StatsResponse["agent_usage"];
rangeLabel: string;
}) {
return (
<>
Completed Tasks Over Time
Most Productive
Activity ({rangeLabel.toLowerCase()})
Top Agents
>
);
}
function StatsContent({ resolvedStats, rangeLabel, workspaceId }: StatsContentProps) {
const {
global,
task_stats,
completed_activity,
daily_activity,
agent_usage,
repository_stats,
git_stats,
} = resolvedStats;
return (
Repository Activity
Top Repositories
Repo Leaders
);
}
function buildStatsSummary(
resolvedStats: StatsResponse,
rangeLabel: string,
completedInRange: number,
): string {
const { global, repository_stats, git_stats } = resolvedStats;
const completion =
global.total_tasks > 0
? `${Math.round((global.completed_tasks / global.total_tasks) * 100)}%`
: "—";
const topRepo = repository_stats
.filter((r) => r.total_tasks > 0)
.sort((a, b) => b.total_tasks - a.total_tasks)[0];
const topRepoLabel = topRepo ? `${topRepo.repository_name} (${topRepo.total_tasks} tasks)` : "—";
const hasGitStats =
git_stats && (git_stats.total_commits > 0 || git_stats.total_files_changed > 0);
const gitLine = hasGitStats
? `${git_stats.total_commits} commits, +${git_stats.total_insertions.toLocaleString()}/-${git_stats.total_deletions.toLocaleString()}`
: "no git activity";
return [
`*KanDev Stats — ${rangeLabel}*`,
`- Tasks: ${global.total_tasks} total (${global.completed_tasks} done, ${global.in_progress_tasks} in progress) · ${completion} completion`,
`- Completed (${rangeLabel}): ${completedInRange}`,
`- Time: ${formatDuration(global.total_duration_ms)} total · ${formatDuration(global.avg_duration_ms_per_task)} avg/task`,
`- Repos: ${repository_stats.length} tracked · Top repo: ${topRepoLabel}`,
`- Git: ${gitLine}`,
].join("\n");
}
export function StatsPageClient({ stats, error, workspaceId, activeRange }: StatsPageClientProps) {
const router = useRouter();
const searchParams = useSearchParams();
const [copied, setCopied] = useState(false);
const range = (activeRange ?? "month") as RangeKey;
const rangeLabel = getRangeLabel(range);
const resolvedStats = stats ?? EMPTY_STATS;
const completedInRange = useMemo(
() =>
(resolvedStats.completed_activity ?? []).reduce((sum, item) => sum + item.completed_tasks, 0),
[resolvedStats.completed_activity],
);
const statsSummary = useMemo(
() => buildStatsSummary(resolvedStats, rangeLabel, completedInRange),
[resolvedStats, rangeLabel, completedInRange],
);
if (!workspaceId) return ;
if (error)
return (
} />
Error loading stats: {error}
);
if (!stats) return ;
const handleCopyStats = async () => {
try {
await navigator.clipboard.writeText(statsSummary);
setCopied(true);
window.setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error("Failed to copy stats summary", err);
}
};
const handleRangeChange = (nextRange: RangeKey) => {
const params = new URLSearchParams(searchParams?.toString() ?? "");
params.set("range", nextRange);
router.push(`/stats?${params.toString()}`);
};
return (
);
}