Files
stack/apps/web/src/components/dashboard/ActivityFeed.tsx
T
jason.woltje 7581d26567 Phase 2: Task Ingestion Pipeline (#459) (#460)
Co-authored-by: Jason Woltje <[email protected]>
Co-committed-by: Jason Woltje <[email protected]>
2026-02-23 00:54:55 +00:00

193 lines
5.3 KiB
TypeScript

import type { ReactElement } from "react";
import { Card, SectionHeader, Badge } from "@mosaic/ui";
import type { RecentActivity } from "@/lib/api/dashboard";
type BadgeVariantType =
| "badge-amber"
| "badge-red"
| "badge-teal"
| "badge-blue"
| "badge-muted"
| "badge-purple"
| "badge-pulse";
interface ActivityDisplayItem {
id: string;
icon: string;
iconBg: string;
title: string;
highlight: string;
rest: string;
timestamp: string;
badge?:
| {
text: string;
variant: BadgeVariantType;
}
| undefined;
}
export interface ActivityFeedProps {
items?: RecentActivity[] | undefined;
}
/* ------------------------------------------------------------------ */
/* Mapping helpers */
/* ------------------------------------------------------------------ */
function getIconForAction(action: string): { icon: string; iconBg: string } {
const lower = action.toLowerCase();
if (lower.includes("complet") || lower.includes("finish") || lower.includes("success")) {
return { icon: "\u2713", iconBg: "rgba(20,184,166,0.15)" };
}
if (lower.includes("fail") || lower.includes("error")) {
return { icon: "\u2717", iconBg: "rgba(229,72,77,0.15)" };
}
if (lower.includes("warn") || lower.includes("limit")) {
return { icon: "\u26A0", iconBg: "rgba(245,158,11,0.15)" };
}
if (lower.includes("start") || lower.includes("creat")) {
return { icon: "\u2191", iconBg: "rgba(47,128,255,0.15)" };
}
if (lower.includes("update") || lower.includes("modif")) {
return { icon: "\u21BB", iconBg: "rgba(139,92,246,0.15)" };
}
return { icon: "\u2022", iconBg: "rgba(100,116,139,0.15)" };
}
function getBadgeForAction(action: string): ActivityDisplayItem["badge"] {
const lower = action.toLowerCase();
if (lower.includes("fail") || lower.includes("error")) {
return { text: "error", variant: "badge-red" };
}
if (lower.includes("warn") || lower.includes("limit")) {
return { text: "warn", variant: "badge-amber" };
}
return undefined;
}
function formatRelativeTime(isoDate: string): string {
const now = Date.now();
const then = new Date(isoDate).getTime();
const diffMs = now - then;
if (Number.isNaN(diffMs) || diffMs < 0) return "just now";
const minutes = Math.floor(diffMs / 60_000);
if (minutes < 1) return "just now";
if (minutes < 60) return `${String(minutes)}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${String(hours)}h ago`;
const days = Math.floor(hours / 24);
return `${String(days)}d ago`;
}
function mapActivityToDisplay(activity: RecentActivity): ActivityDisplayItem {
const { icon, iconBg } = getIconForAction(activity.action);
return {
id: activity.id,
icon,
iconBg,
title: "",
highlight: activity.entityType,
rest: ` ${activity.action} (${activity.entityId})`,
timestamp: formatRelativeTime(activity.createdAt),
badge: getBadgeForAction(activity.action),
};
}
/* ------------------------------------------------------------------ */
/* Components */
/* ------------------------------------------------------------------ */
interface ActivityItemRowProps {
item: ActivityDisplayItem;
}
function ActivityItemRow({ item }: ActivityItemRowProps): ReactElement {
return (
<div
style={{
display: "flex",
alignItems: "flex-start",
gap: 12,
padding: "10px 0",
borderBottom: "1px solid var(--border)",
}}
>
<div
style={{
width: 28,
height: 28,
borderRadius: 6,
flexShrink: 0,
background: item.iconBg,
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "0.8rem",
color: "var(--text)",
}}
>
{item.icon}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div
style={{
fontSize: "0.8rem",
color: "var(--text-2)",
lineHeight: 1.4,
}}
>
{item.title}
<strong style={{ color: "var(--text)" }}>{item.highlight}</strong>
{item.rest}
{item.badge !== undefined && (
<Badge variant={item.badge.variant} style={{ marginLeft: 6 }}>
{item.badge.text}
</Badge>
)}
</div>
<div
style={{
fontSize: "0.7rem",
fontFamily: "var(--mono)",
color: "var(--muted)",
marginTop: 2,
}}
>
{item.timestamp}
</div>
</div>
</div>
);
}
export function ActivityFeed({ items }: ActivityFeedProps): ReactElement {
const displayItems = items ? items.map(mapActivityToDisplay) : [];
return (
<Card>
<SectionHeader title="Activity Feed" subtitle="Recent agent events" />
<div>
{displayItems.length > 0 ? (
displayItems.map((item) => <ActivityItemRow key={item.id} item={item} />)
) : (
<div
style={{
padding: "24px 0",
textAlign: "center",
fontSize: "0.8rem",
color: "var(--muted)",
}}
>
No recent activity
</div>
)}
</div>
</Card>
);
}