import InboxCard from "@/components/InboxCard";
import { useI18n } from "@/i18n/I18nProvider";
import { ackEvent, listEventsWithAck, unackEvent } from "@/lib/queries";
import { useRealtimeWorkerEvents } from "@/lib/realtime";
import type { WorkerEvent } from "@/types";
import { useCallback, useEffect, useState } from "react";
const ACTIONABLE_SEVS = ["suggest", "warn", "block"] as const;
export default function Inbox() {
const { t } = useI18n();
const [events, setEvents] = useState<WorkerEvent[]>([]);
const [includeAcked, setIncludeAcked] = useState(false);
const [includeInfo, setIncludeInfo] = useState(false);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let alive = true;
setLoading(true);
listEventsWithAck({
eventType: "advice",
limit: 100,
includeAcked,
severities: includeInfo
? [...ACTIONABLE_SEVS, "info"]
: [...ACTIONABLE_SEVS],
})
.then((r) => alive && setEvents(r))
.catch((e) => alive && setError(String(e?.message ?? e)))
.finally(() => alive && setLoading(false));
return () => {
alive = false;
};
}, [includeAcked, includeInfo]);
useRealtimeWorkerEvents({
onInsert: (ev) => {
if (ev.event_type !== "advice") return;
if (!includeInfo && ev.severity === "info") return;
setEvents((prev) => [ev, ...prev]);
},
onUpdate: (ev) => {
setEvents((prev) =>
prev.map((e) => (e.id === ev.id ? { ...e, ...ev } : e)),
);
},
});
const handleAck = useCallback(async (ev: WorkerEvent) => {
// optimistic
setEvents((prev) =>
prev.map((e) =>
e.id === ev.id ? { ...e, acked_at: new Date().toISOString() } : e,
),
);
try {
await ackEvent(ev.id);
} catch (err) {
setError(String((err as Error)?.message ?? err));
}
}, []);
const handleUnack = useCallback(async (ev: WorkerEvent) => {
setEvents((prev) =>
prev.map((e) => (e.id === ev.id ? { ...e, acked_at: null } : e)),
);
try {
await unackEvent(ev.id);
} catch (err) {
setError(String((err as Error)?.message ?? err));
}
}, []);
const visible = includeAcked ? events : events.filter((e) => !e.acked_at);
return (
<div className="p-6 space-y-4">
<header className="flex items-end justify-between">
<h1 className="text-xl font-semibold">{t("inbox.title")}</h1>
<div className="flex items-center gap-4">
<label className="flex items-center gap-2 text-xs text-muted-foreground">
<input
type="checkbox"
checked={includeInfo}
onChange={(e) => setIncludeInfo(e.target.checked)}
/>
{t("inbox.showInfo")}
</label>
<label className="flex items-center gap-2 text-xs text-muted-foreground">
<input
type="checkbox"
checked={includeAcked}
onChange={(e) => setIncludeAcked(e.target.checked)}
/>
{t("inbox.showAcked")}
</label>
</div>
</header>
{error && (
<div className="rounded-md border border-red-300 bg-red-50 p-3 text-sm text-red-700">
{error}
</div>
)}
<div className="rounded-md border bg-card">
{loading && <div className="p-4 text-sm text-muted-foreground">{t("common.loading")}</div>}
{!loading && visible.length === 0 && (
<div className="p-4 text-sm text-muted-foreground">
{includeAcked ? t("inbox.empty.none") : t("inbox.empty.zero")}{" "}
{t("inbox.empty.hint")}
</div>
)}
{visible.map((ev) => (
<InboxCard
key={ev.id}
event={ev}
onAck={handleAck}
onUnack={handleUnack}
/>
))}
</div>
</div>
);
}