/** * dashboard-client/src/tabs/WikiTab.tsx — Wiki Revival (W3): wiki landing list. * * Replaces TopicsTab in the App registry (TopicsTab.tsx kept, unused). Renders * the wiki index: each auto-categorized topic with its resolved label (edited * badge when user-renamed), member count, curation badges (label/merge/split), * and a click-to-open that pushes a client-side page state to WikiPage. * * Styling is Tailwind + shadcn (Card, Badge, Button) — no legacy CSS classes. */ import type React from "react"; import { useCallback, useMemo, useState } from "react"; import { useApi } from "../hooks/useApi"; import { fetchWikiIndex } from "../api/client"; import type { WikiIndexEntry, WikiIndexResponse } from "@contracts"; import { Badge } from "../components/ui/badge"; import { Button } from "../components/ui/button"; import { Toggle } from "../components/ui/toggle"; import WikiPage from "./WikiTab/WikiPage"; import TopicEvolutionView from "./WikiTab/TopicEvolutionView"; type WikiView = "list" | "evolution"; const KIND_BADGES: Record = { label: "accent", merge: "warning", split: "default", }; function fmtTs(ms: number | null): string { if (!ms) return "—"; return new Date(ms).toLocaleString(); } function ViewToggle({ view, onChange, }: { view: WikiView; onChange: (v: WikiView) => void; }): React.ReactElement { return (
onChange("list")}> Topics onChange("evolution")} > Evolution
); } interface RowProps { entry: WikiIndexEntry; onOpen: (id: string) => void; } function TopicRow({ entry, onOpen }: RowProps): React.ReactElement { return (
{entry.label} {entry.edited && edited}
{entry.memoryCount}
{entry.overrideKinds.map((k) => ( {k} ))}
{fmtTs(entry.lastUpdated)} ); } export default function WikiTab(): React.ReactElement { const { data, loading, error } = useApi( useCallback(() => fetchWikiIndex(), []), { pollInterval: 30_000 }, ); const [view, setView] = useState("list"); const [query, setQuery] = useState(""); const [openId, setOpenId] = useState(null); const filtered = useMemo(() => { if (!data) return []; const q = query.trim().toLowerCase(); if (!q) return data.topics; return data.topics.filter((t) => t.label.toLowerCase().includes(q), ); }, [data, query]); if (openId) { return setOpenId(null)} />; } if (view === "evolution") { return (
); } if (error && !data) { return (
Error loading wiki: {error.message}
); } if (loading && !data) { return (
Loading wiki…
); } if (!data) { return (
No wiki data available.
); } if (data.totalTopics === 0) { return (

Wiki

No topics yet. Topics are auto-generated after every Nth compaction from real memory embeddings (k-means + TF-IDF).

Check back after a few more compaction cycles.

); } return (

Wiki

{data.totalTopics} topic{data.totalTopics !== 1 ? "s" : ""} ·{" "} {data.totalMemories} assigned memori {data.totalMemories !== 1 ? "es" : "y"} {data.lastRebuildAt != null && ( <> · last rebuild {fmtTs(data.lastRebuildAt)} )}

setQuery(e.target.value)} className="w-64 rounded-md border border-border bg-bg-elevated/50 px-3 py-2 text-sm outline-none transition-colors focus:border-primary" />
{filtered.map((t) => ( ))} {filtered.length === 0 && ( )}
Label Memories Badges Last updated
No topics match "{query}".
); }