import { useId, useState, type ReactNode } from "react"; import { ChevronDown, CircleAlert, CircleCheck, Info, OctagonAlert, TriangleAlert, type LucideIcon, } from "lucide-react"; import { cn } from "@/lib/utils"; export type SystemNoticeTone = "neutral" | "info" | "success" | "warning" | "danger"; export type SystemNoticeMetadataRow = | { kind: "text"; label: string; value: string } | { kind: "code"; label: string; value: string } | { kind: "issue"; label: string; identifier: string; href?: string; title?: string } | { kind: "agent"; label: string; name: string; href?: string } | { kind: "run"; label: string; runId: string; href?: string; status?: string }; export type SystemNoticeMetadataSection = { title?: string; rows: SystemNoticeMetadataRow[]; }; export type SystemNoticeProps = { tone?: SystemNoticeTone; /** Short label that names the system actor + tone, e.g. "System warning". Required so tone is not color-only. */ label?: string; /** Short visible body — one or two sentences from the system perspective. */ body: ReactNode; /** Optional small chip for the originating run link. */ source?: { label: string; href?: string }; /** Hidden-by-default metadata. Renders the Details affordance only when present. */ metadata?: SystemNoticeMetadataSection[]; /** Force the details panel open initially. Defaults to false (collapsed). */ detailsDefaultOpen?: boolean; /** Optional ISO timestamp shown next to the label. */ timestamp?: string; className?: string; }; type ToneTokens = { container: string; iconWrap: string; icon: LucideIcon; iconClass: string; label: string; divider: string; }; const TONE_TOKENS: Record = { neutral: { container: "border-border bg-muted/35 dark:bg-muted/20", iconWrap: "bg-muted text-foreground/70", icon: Info, iconClass: "text-muted-foreground", label: "text-muted-foreground", divider: "border-border/70", }, info: { container: "border-sky-300/70 bg-sky-50/70 dark:border-sky-500/30 dark:bg-sky-500/10", iconWrap: "bg-sky-100 text-sky-700 dark:bg-sky-500/20 dark:text-sky-200", icon: Info, iconClass: "text-sky-700 dark:text-sky-300", label: "text-sky-800 dark:text-sky-200", divider: "border-sky-300/50 dark:border-sky-500/30", }, success: { container: "border-emerald-300/70 bg-emerald-50/70 dark:border-emerald-500/30 dark:bg-emerald-500/10", iconWrap: "bg-emerald-100 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-200", icon: CircleCheck, iconClass: "text-emerald-700 dark:text-emerald-300", label: "text-emerald-800 dark:text-emerald-200", divider: "border-emerald-300/50 dark:border-emerald-500/30", }, warning: { container: "border-amber-300/70 bg-amber-50/80 dark:border-amber-500/30 dark:bg-amber-500/10", iconWrap: "bg-amber-100 text-amber-800 dark:bg-amber-500/20 dark:text-amber-200", icon: TriangleAlert, iconClass: "text-amber-700 dark:text-amber-300", label: "text-amber-900 dark:text-amber-200", divider: "border-amber-300/60 dark:border-amber-500/30", }, danger: { container: "border-red-400/60 bg-red-50/80 dark:border-red-500/35 dark:bg-red-500/10", iconWrap: "bg-red-100 text-red-700 dark:bg-red-500/20 dark:text-red-200", icon: OctagonAlert, iconClass: "text-red-700 dark:text-red-300", label: "text-red-900 dark:text-red-200", divider: "border-red-400/50 dark:border-red-500/30", }, }; function formatTimestamp(ts: string) { try { return new Date(ts).toLocaleString(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit", }); } catch { return ts; } } function MetadataRow({ row, tone }: { row: SystemNoticeMetadataRow; tone: ToneTokens }) { return (
{row.label}
{(() => { switch (row.kind) { case "text": return {row.value}; case "code": return ( {row.value} ); case "issue": { const issueLabel = ( <> {row.identifier} {row.title ? ( — {row.title} ) : null} ); if (row.href) { return ( {issueLabel} ); } return ( {issueLabel} ); } case "agent": if (row.href) { return ( {row.name} ); } return ( {row.name} ); case "run": { const runShort = row.runId.length > 12 ? `${row.runId.slice(0, 8)}…` : row.runId; const inner = ( <> {runShort} {row.status ? ( {row.status} ) : null} ); if (row.href) { return ( {inner} ); } return ( {inner} ); } } })()}
); } export function SystemNotice({ tone = "neutral", label, body, source, metadata, detailsDefaultOpen = false, timestamp, className, }: SystemNoticeProps) { const tokens = TONE_TOKENS[tone]; const ToneIcon = tokens.icon; const [open, setOpen] = useState(detailsDefaultOpen); const detailsId = useId(); const hasDetails = Boolean(metadata && metadata.length > 0); const resolvedLabel = label ?? { neutral: "System notice", info: "System notice", success: "System notice", warning: "System warning", danger: "System alert", }[tone]; return (
{resolvedLabel} {source ? ( <> · {source.href ? ( {source.label} ) : ( {source.label} )} ) : null} {timestamp ? ( <> · {formatTimestamp(timestamp)} ) : null}
{body}
{hasDetails ? ( ) : null}
{hasDetails && open ? (
{metadata!.map((section, sectionIdx) => (
{section.title ? (
{section.title}
) : null}
{section.rows.map((row, rowIdx) => ( ))}
))}
) : null}
); } export default SystemNotice;