import { createFileRoute, Link } from "@tanstack/react-router"; import { useEffect, useState } from "react"; import { api, type ContentDB } from "@/lib/api"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "@/components/ui/tooltip"; import { timeAgo, dateGroup, parseSourceLabel } from "@/components/analytics"; import { Database, Layers, Clock, Trash2, ChevronRight } from "lucide-react"; export const Route = createFileRoute("/knowledge")({ component: KnowledgeBase }); interface GroupedSources { label: string; sources: { source: ContentDB["sources"][0]; dbHash: string }[]; totalChunks: number; } function KnowledgeBase() { const [dbs, setDbs] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { api.content().then(d => { setDbs(d); setLoading(false); }); }, []); const handleDelete = async (e: React.MouseEvent, dbHash: string, sourceId: number) => { e.preventDefault(); e.stopPropagation(); if (!confirm("Delete this source and all its chunks?")) return; await api.deleteSource(dbHash, sourceId); api.content().then(setDbs); }; if (loading) return

Loading knowledge base...

; const nonEmpty = dbs.filter(db => db.sourceCount > 0); // Compute KPI values const totalSources = nonEmpty.reduce((a, db) => a + db.sourceCount, 0); const totalChunks = nonEmpty.reduce((a, db) => a + db.chunkCount, 0); // Find freshest indexed_at across all sources let freshestDate: string | null = null; for (const db of nonEmpty) { for (const s of db.sources) { if (s.indexedAt && (!freshestDate || s.indexedAt > freshestDate)) { freshestDate = s.indexedAt; } } } // Flatten all sources with their dbHash, then group by date const allSources: { source: ContentDB["sources"][0]; dbHash: string }[] = []; for (const db of nonEmpty) { for (const s of db.sources) { allSources.push({ source: s, dbHash: db.hash }); } } // Sort by indexedAt descending allSources.sort((a, b) => (b.source.indexedAt || "").localeCompare(a.source.indexedAt || "")); // Group by date const groupOrder = ["Today", "Yesterday", "This Week", "Older"]; const groupMap = new Map(); for (const g of groupOrder) groupMap.set(g, { label: g, sources: [], totalChunks: 0 }); for (const item of allSources) { const g = dateGroup(item.source.indexedAt); const group = groupMap.get(g)!; group.sources.push(item); group.totalChunks += item.source.chunks; } const groups = groupOrder.map(g => groupMap.get(g)!).filter(g => g.sources.length > 0); if (nonEmpty.length === 0) { return (

Knowledge Base

No indexed content yet

Use ctx_index or ctx_fetch_and_index to add content.
); } return (

Knowledge Base

{nonEmpty.length} database{nonEmpty.length > 1 ? "s" : ""} with indexed content

{/* KPI Strip */}
Sources
{totalSources}

across {nonEmpty.length} db{nonEmpty.length > 1 ? "s" : ""}

Chunks
{totalChunks}

{totalSources > 0 ? `~${Math.round(totalChunks / totalSources)} per source` : "no sources"}

Freshness
{timeAgo(freshestDate)}

last indexed

{/* Source groups by date */} {groups.map(group => (

{group.label}

{group.sources.length} source{group.sources.length > 1 ? "s" : ""} {group.totalChunks} chunks
{group.sources.map(({ source: s, dbHash }) => { const labels = parseSourceLabel(s.label); return (
{/* Source labels as badges */}
{labels.map((lbl, i) => ( {lbl} ))} {parseSourceLabel(s.label).length < s.label.split(",").length && ( +{s.label.split(",").length - 3} more )}
{s.label} {s.label}
{/* Chunk counts */}
{s.chunks} chunk{s.chunks !== 1 ? "s" : ""} {s.codeChunks > 0 && ( {s.codeChunks} code )}
{/* Time ago */} {timeAgo(s.indexedAt)} {/* Delete button */}
); })}
))}
); }