/** * dashboard-client/src/tabs/WikiTab/TopicTimeline.tsx — per-topic timeline. * * Renders topic memory-addition buckets as a lightweight SVG bar strip (no * chart library needed). X = time bucket, bar height = memories added. * Fetches GET /api/wiki/topic/:topicId/timeline. * * Styling: Tailwind + shadcn (Card). No legacy CSS classes. */ import type React from "react"; import { useCallback } from "react"; import { useApi } from "../../hooks/useApi"; import { fetchTopicTimeline } from "../../api/client"; import type { TopicTimelineResponse } from "@contracts"; const W = 720; const H = 56; const BAR_W = 10; const GAP = 3; interface Props { topicId: string; } function fmtBucket(ts: number): string { if (!ts) return "—"; return new Date(ts).toLocaleDateString(); } export default function TopicTimeline({ topicId }: Props): React.ReactElement { const { data, error } = useApi( useCallback(() => fetchTopicTimeline(topicId), [topicId]), {}, ); if (error) { return (
Timeline unavailable.
); } if (!data) { return (
Loading timeline…
); } const { buckets, total } = data; const max = Math.max(...buckets.map((b) => b.count), 1); const totalWidth = buckets.length * (BAR_W + GAP); const scale = Math.min(1, (W - 24) / Math.max(totalWidth, 1)); return (

Memory timeline{" "} ({total} memory{total !== 1 ? "ies" : "y"} · daily buckets)

{buckets.length === 0 ? (

No timeline data yet.

) : ( <> {buckets.map((b, i) => { const x = i * (BAR_W + GAP) * scale; const h = Math.max(3, (b.count / max) * (H - 20)); return ( {`${fmtBucket(b.bucket)}: ${b.count}`} ); })}

First display: {fmtBucket(buckets[0].bucket)} · last: {fmtBucket(buckets[buckets.length - 1].bucket)}

)}
); }