"use client"; import { ArrowUpRight, Bot, Clock, KeyRound } from "lucide-react"; import { useQuery } from "@tanstack/react-query"; import Link from "next/link"; import { getLatestBotReport, type BotReportRead } from "@/lib/api/bot"; import { cn } from "@/lib/utils"; import { sectionRail, sectionTone, toneIcon, type MetricToneKey, type SectionToneKey, } from "./tokens"; function timeAgo(iso: string): string { // Backend returns naive UTC datetimes without 'Z'; append it so the browser // parses correctly instead of treating the string as local time. const utc = /Z|[+-]\d{2}:?\d{2}$/.test(iso) ? iso : `${iso}Z`; const diff = Math.floor((Date.now() - new Date(utc).getTime()) / 1000); if (diff < 5) return "just now"; if (diff < 60) return `${diff}s ago`; if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; return `${Math.floor(diff / 86400)}d ago`; } type BotStatusTone = { label: string; tone: SectionToneKey; iconTone: MetricToneKey | "neutral"; badgeClassName: string; dotClassName: string; message: string; }; function getStatusTone(report: BotReportRead | null): BotStatusTone { const status = report?.status?.toLowerCase(); if (status === "idle") { return { label: "Idle", tone: "success", iconTone: "success", badgeClassName: "border-[color:rgba(52,211,153,0.28)] bg-[color:rgba(52,211,153,0.14)] text-[color:var(--success)]", dotClassName: "bg-[color:var(--success)] shadow-[0_0_0_3px_rgba(52,211,153,0.14)]", message: "Ready for work", }; } if (status === "busy") { return { label: "Busy", tone: "warning", iconTone: "warning", badgeClassName: "border-[color:rgba(251,191,36,0.3)] bg-[color:rgba(251,191,36,0.14)] text-[color:var(--warning)]", dotClassName: "bg-[color:var(--warning)] shadow-[0_0_0_3px_rgba(251,191,36,0.14)]", message: "Working now", }; } if (status === "error") { return { label: "Error", tone: "danger", iconTone: "danger", badgeClassName: "border-[color:rgba(248,113,113,0.3)] bg-[color:rgba(248,113,113,0.12)] text-[color:var(--danger)]", dotClassName: "bg-[color:var(--danger)] shadow-[0_0_0_3px_rgba(248,113,113,0.14)]", message: "Needs attention", }; } if (report?.status) { return { label: report.status, tone: "accent", iconTone: "accent", badgeClassName: "border-[color:rgba(96,165,250,0.28)] bg-[color:rgba(96,165,250,0.13)] text-[color:var(--accent-strong)]", dotClassName: "bg-[color:var(--accent-strong)] shadow-[0_0_0_3px_rgba(96,165,250,0.14)]", message: "Status reported", }; } return { label: "Offline", tone: "neutral", iconTone: "neutral", badgeClassName: "border-[color:var(--border)] bg-[color:var(--surface-strong)] text-muted", dotClassName: "bg-[color:var(--text-muted)] shadow-[0_0_0_3px_rgba(148,163,184,0.12)]", message: "Awaiting first report", }; } function StatusBadge({ statusTone }: { statusTone: BotStatusTone }) { return ( {statusTone.label} ); } function Banner({ report }: { report: BotReportRead | null }) { const statusTone = getStatusTone(report); const target = report ? [report.project ?? "No project", report.task].filter(Boolean).join(" / ") : "Ripley has not reported in yet."; return (
Ripley
{statusTone.message}
{target}