Files
roboco/panel/src/components/knowledge-base/kb-stats-card.tsx
T
Renn F 57254b3ce2 feat(panel): dense tooltip pass — projects, products, social, KB, business, settings, notifications (178 tips)
Density bar: every dialog field, column header, filter, badge, and
status pill explains itself — grounded in the backend (ladder
normalization, rate-limit park/probe, self-hosted fallback, reindex
scope). Disabled buttons span-wrap so tips fire; tabs re-assert
data-state.
2026-07-15 17:56:12 +02:00

155 lines
5.2 KiB
TypeScript

"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { HelpTip } from "@/components/ui/help-tip";
import { getIndexTypeDescription } from "./kb-index-type-badge";
import { KBStats, KBIndexType } from "@/types";
import {
Database,
FileText,
MessageSquare,
BookOpen,
AlertTriangle,
Scale,
GitBranch,
ClipboardCheck,
Lightbulb,
ScrollText,
StickyNote,
} from "lucide-react";
import { formatDistanceToNow } from "date-fns";
const indexIcons: Record<KBIndexType, React.ReactNode> = {
[KBIndexType.DOCUMENTATION]: <FileText className="h-4 w-4 text-blue-500" />,
[KBIndexType.CONVERSATIONS]: (
<MessageSquare className="h-4 w-4 text-green-500" />
),
[KBIndexType.JOURNALS]: <BookOpen className="h-4 w-4 text-orange-500" />,
[KBIndexType.ERRORS]: <AlertTriangle className="h-4 w-4 text-red-500" />,
[KBIndexType.STANDARDS]: <Scale className="h-4 w-4 text-cyan-500" />,
[KBIndexType.DECISIONS]: <GitBranch className="h-4 w-4 text-indigo-500" />,
[KBIndexType.REVIEWS]: <ClipboardCheck className="h-4 w-4 text-pink-500" />,
[KBIndexType.LEARNINGS]: <Lightbulb className="h-4 w-4 text-yellow-500" />,
[KBIndexType.PLAYBOOKS]: <ScrollText className="h-4 w-4 text-emerald-500" />,
[KBIndexType.VAULT_NOTES]: <StickyNote className="h-4 w-4 text-violet-500" />,
};
const indexLabels: Record<KBIndexType, string> = {
[KBIndexType.DOCUMENTATION]: "Docs",
[KBIndexType.CONVERSATIONS]: "Convos",
[KBIndexType.JOURNALS]: "Journals",
[KBIndexType.ERRORS]: "Errors",
[KBIndexType.STANDARDS]: "Standards",
[KBIndexType.DECISIONS]: "Decisions",
[KBIndexType.REVIEWS]: "Reviews",
[KBIndexType.LEARNINGS]: "Learnings",
[KBIndexType.PLAYBOOKS]: "Playbooks",
[KBIndexType.VAULT_NOTES]: "Vault Notes",
};
interface KBStatsCardProps {
stats: KBStats | undefined;
isLoading: boolean;
}
export function KBStatsCard({ stats, isLoading }: KBStatsCardProps) {
if (isLoading) {
return (
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<Database className="h-4 w-4" />
Index Stats
</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="flex items-center justify-between">
<Skeleton className="h-4 w-16" />
<Skeleton className="h-4 w-10" />
</div>
))}
</CardContent>
</Card>
);
}
if (!stats || !Array.isArray(stats.indexes)) {
return (
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<Database className="h-4 w-4" />
Index Stats
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">No data available</p>
</CardContent>
</Card>
);
}
const latestUpdated = stats.indexes.reduce<string | null>(
(acc, idx) =>
idx.last_updated && (!acc || idx.last_updated > acc)
? idx.last_updated
: acc,
null,
);
return (
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<Database className="h-4 w-4" />
Index Stats
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{stats.indexes.map((idx) => (
<div
key={idx.index_type}
className="flex items-center justify-between text-sm"
>
<HelpTip label={getIndexTypeDescription(idx.index_type)}>
<div className="flex items-center gap-2 w-fit">
{indexIcons[idx.index_type]}
<span>{indexLabels[idx.index_type]}</span>
</div>
</HelpTip>
<div className="text-right">
<span className="font-medium">
{idx.document_count.toLocaleString()}
</span>
<span className="text-muted-foreground text-xs ml-1">docs</span>
</div>
</div>
))}
<div className="pt-2 border-t">
<div className="flex items-center justify-between text-sm">
<span className="font-medium">Total</span>
<span className="font-bold">
{stats.total_documents.toLocaleString()}
</span>
</div>
<div className="flex items-center justify-between text-xs text-muted-foreground mt-1">
<HelpTip label="A chunk is a segment of a document split for embedding — one document can produce many chunks">
<span className="w-fit">Chunks</span>
</HelpTip>
<span>{stats.total_chunks.toLocaleString()}</span>
</div>
</div>
{latestUpdated && (
<HelpTip label={new Date(latestUpdated).toLocaleString()}>
<p className="text-xs text-muted-foreground pt-1 w-fit">
Updated {formatDistanceToNow(new Date(latestUpdated))} ago
</p>
</HelpTip>
)}
</CardContent>
</Card>
);
}