import { useState } from "react";
import {
CalendarClock,
CircleAlert,
ListTodo,
RefreshCcw,
} from "lucide-react";
import type { TFunction } from "i18next";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useSessionAutomationJobs } from "@/hooks/useSessionAutomationJobs";
import { currentLocale } from "@/i18n";
import { fmtDateTime } from "@/lib/format";
import type { SessionAutomationJob } from "@/lib/types";
import { cn } from "@/lib/utils";
const RELATIVE_THRESHOLDS: [number, Intl.RelativeTimeFormatUnit][] = [
[60, "second"],
[60, "minute"],
[24, "hour"],
[7, "day"],
[4.345, "week"],
[12, "month"],
[Number.POSITIVE_INFINITY, "year"],
];
interface SessionInfoPopoverProps {
sessionKey: string;
token: string;
title: string;
}
export function SessionInfoPopover({ sessionKey, token, title }: SessionInfoPopoverProps) {
const { t } = useTranslation("common");
const [open, setOpen] = useState(false);
const { jobs, loading, loadFailed, now } = useSessionAutomationJobs(open, token, sessionKey);
const automationContent = loading ? (
{t("thread.sessionInfo.loading")}
) : loadFailed ? (
{t("thread.sessionInfo.loadFailed")}
) : jobs.length ? (
) : (
{t("thread.sessionInfo.empty")}
);
return (
{t("thread.sessionInfo.title")}
{title || t("thread.sessionInfo.untitled")}
{t("thread.sessionInfo.automations")}
{t("thread.sessionInfo.count", { count: jobs.length })}
{automationContent}
);
}
function AutomationRow({ job, now }: { job: SessionAutomationJob; now: number }) {
const { t } = useTranslation("common");
const schedule = formatSchedule(job, t);
const nextRun = formatNextRun(job, t, now);
const statusClass = job.enabled
? job.state.last_status === "error"
? "bg-destructive"
: "bg-emerald-500"
: "bg-muted-foreground/35";
return (
{job.name}
{!job.enabled ? (
{t("thread.sessionInfo.disabled")}
) : null}
{job.payload.message}
{schedule}
ยท
{nextRun.label}
);
}
function formatSchedule(job: SessionAutomationJob, t: TFunction) {
const locale = currentLocale();
if (isLocalTriggerAutomation(job)) {
return t("thread.sessionInfo.schedule.local");
}
if (job.schedule.kind === "at" && job.schedule.at_ms) {
return t("thread.sessionInfo.schedule.at", { time: fmtDateTime(job.schedule.at_ms, locale) });
}
if (job.schedule.kind === "every" && job.schedule.every_ms) {
return t("thread.sessionInfo.schedule.every", {
duration: formatDuration(job.schedule.every_ms, locale),
});
}
if (job.schedule.kind === "cron" && job.schedule.expr) {
return job.schedule.tz
? t("thread.sessionInfo.schedule.cronWithTz", {
expr: job.schedule.expr,
tz: job.schedule.tz,
})
: t("thread.sessionInfo.schedule.cron", { expr: job.schedule.expr });
}
return t("thread.sessionInfo.schedule.unknown");
}
function formatNextRun(job: SessionAutomationJob, t: TFunction, now: number) {
const locale = currentLocale();
if (!job.enabled) {
return { label: t("thread.sessionInfo.next.disabled"), title: "" };
}
if (job.state.pending) {
return { label: t("thread.sessionInfo.next.pending"), title: "" };
}
if (isLocalTriggerAutomation(job)) {
return { label: t("thread.sessionInfo.next.local"), title: "" };
}
const next = job.state.next_run_at_ms;
if (!next) {
return { label: t("thread.sessionInfo.next.none"), title: "" };
}
return {
label: t("thread.sessionInfo.next.label", { time: relativeTimeFrom(next, now, locale) }),
title: fmtDateTime(next, locale),
};
}
function isLocalTriggerAutomation(job: SessionAutomationJob): boolean {
return job.kind === "local_trigger"
|| job.payload.kind === "local_trigger"
|| job.schedule.kind === "local";
}
function relativeTimeFrom(value: number, now: number, locale: string): string {
let delta = (value - now) / 1000;
const formatter = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
for (const [step, unit] of RELATIVE_THRESHOLDS) {
if (Math.abs(delta) < step) {
return formatter.format(Math.round(delta), unit);
}
delta /= step;
}
return formatter.format(Math.round(delta), "year");
}
function formatDuration(ms: number, locale: string): string {
const units: Array<[Intl.NumberFormatOptions["unit"], number]> = [
["day", 86_400_000],
["hour", 3_600_000],
["minute", 60_000],
["second", 1000],
];
for (const [unit, size] of units) {
if (ms >= size && ms % size === 0) {
return new Intl.NumberFormat(locale, {
style: "unit",
unit,
unitDisplay: "long",
maximumFractionDigits: 0,
}).format(ms / size);
}
}
return new Intl.NumberFormat(locale, {
style: "unit",
unit: "minute",
unitDisplay: "long",
maximumFractionDigits: 1,
}).format(ms / 60_000);
}