Comment and annotate Two people looking at the same spike, talking about it in place. A comment anchored to a timestamp survives the conversation; the same words in a chat thread are unfindable a month later. npx shadcn@latest add avatar textarea button npm i recharts Tokens this needs: --card, --popover, --popover-foreground, --muted-foreground, --border, --chart-1 ──────────────────────────────────────────────────────────────────────── // PanelComments.tsx ──────────────────────────────────────────────────────────────────────── import { useState } from "react"; import { Line, LineChart, ReferenceArea, XAxis, YAxis } from "recharts"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; /** * A note is anchored to a panel and a span of time, both required. A comment * on "the dashboard" is nearly useless because the dashboard changes * underneath it; the panel keeps the what and the range keeps the when. * `panel` is the panel's key, which outlives its title. */ export type Anchor = { panel: string; from: number; to: number }; /** * Two kinds, and the set is closed. A message is addressed to a person now: * notified, then resolved and out of the way. A record is addressed to * whoever comes next and stays. Building one and hoping it serves as the * other leaves four years of resolved conversations on the page. */ export type Note = { id: string; kind: "message" | "record"; text: string; author: string; at: number; anchor: Anchor; resolved?: boolean; }; export type Sample = { t: number; value: number }; const hhmm = (t: number) => { const d = new Date(t); return `${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}`; }; const ago = (t: number, now: number) => { const m = Math.round((now - t) / 60_000); if (m < 1) return "just now"; if (m < 60) return `${m} min ago`; const h = Math.round(m / 60); if (h < 24) return `${h} h ago`; const d = Math.round(h / 24); if (d < 14) return `${d} days ago`; return `${Math.round(d / 7)} weeks ago`; }; const LINE = "hsl(var(--chart-1))"; export function PanelComments({ panel, data, notes, now, onResolve, onAdd, width = 320, height = 130 }: { panel: string; data: Sample[]; notes: Note[]; /** Epoch ms. Pass one to render on a server. */ now: number; onResolve: (id: string) => void; /** A reply is anchored to the same panel and range as the note it answers. */ onAdd: (text: string, kind: Note["kind"], anchor: Anchor) => void; width?: number; height?: number; }) { const [draft, setDraft] = useState(""); const open = notes.filter((n) => !n.resolved); const anchor = open[0]?.anchor; return (
{panel}
{hhmm(n.anchor.from)}-{hhmm(n.anchor.to)} · {n.anchor.panel}
)}{n.text}