128 lines
4.5 KiB
TypeScript
128 lines
4.5 KiB
TypeScript
import { i18n } from "@lingui/core";
|
|
import { t } from "@lingui/core/macro";
|
|
import { Trans, useLingui } from "@lingui/react/macro";
|
|
import type { RunActivityRow } from "@rakazo/contracts";
|
|
import { runChatTitle } from "@rakazo/core";
|
|
import { isNeedsYouStatus, runActivityLabel } from "../lib/agent-activity";
|
|
import { useRunActivity } from "../lib/run-activity";
|
|
|
|
function statusTone(status: RunActivityRow["status"]): string {
|
|
if (status === "failed") return "text-destructive";
|
|
if (status === "cancelled") return "text-muted-foreground";
|
|
if (status === "completed") return "text-success";
|
|
if (isNeedsYouStatus(status)) return "text-warning";
|
|
return "text-foreground";
|
|
}
|
|
|
|
type ActivityListProps = {
|
|
onOpenRun: (run: RunActivityRow) => void;
|
|
};
|
|
|
|
export function ActivityList({ onOpenRun }: ActivityListProps) {
|
|
const { active: activeRuns, recent: recentRuns, loaded } = useRunActivity();
|
|
const loading = !loaded;
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="px-2.5 py-2 text-ui-body-sm text-muted-foreground/80">
|
|
<Trans>Loading activity…</Trans>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (activeRuns.length === 0 && recentRuns.length === 0) {
|
|
return (
|
|
<div className="mb-2 border-b border-border px-2.5 py-2 text-ui-body-sm text-muted-foreground/80">
|
|
<Trans>No recent activity</Trans>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="mb-2 border-b border-border pb-2">
|
|
{activeRuns.length > 0 ? (
|
|
<section>
|
|
<div className="px-2.5 pb-1 pt-1 text-ui-body-sm font-medium text-muted-foreground/80">
|
|
<Trans>Now</Trans>
|
|
</div>
|
|
{activeRuns.map((run) => (
|
|
<ActivityRow key={run.runId} run={run} onOpen={() => onOpenRun(run)} />
|
|
))}
|
|
</section>
|
|
) : null}
|
|
{recentRuns.length > 0 ? (
|
|
<section className={activeRuns.length > 0 ? "mt-2" : undefined}>
|
|
<div className="px-2.5 pb-1 pt-1 text-ui-body-sm font-medium text-muted-foreground/80">
|
|
<Trans>Recent</Trans>
|
|
</div>
|
|
{recentRuns.map((run) => (
|
|
<ActivityRow key={run.runId} run={run} onOpen={() => onOpenRun(run)} />
|
|
))}
|
|
</section>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function ActivityRow({ run, onOpen }: { run: RunActivityRow; onOpen: () => void }) {
|
|
const { t } = useLingui();
|
|
const title = runChatTitle(run);
|
|
const label = statusLabel(run.status);
|
|
const activityLabel = t`${title}, ${label}`;
|
|
const tone = statusTone(run.status);
|
|
return (
|
|
<button
|
|
type="button"
|
|
aria-label={activityLabel}
|
|
onClick={onOpen}
|
|
className="flex w-full gap-3 rounded-xl px-2.5 py-[9px] text-left hover:bg-accent"
|
|
>
|
|
<span
|
|
className={`mt-1.5 size-2 shrink-0 rounded-full bg-current ${tone}`}
|
|
aria-hidden="true"
|
|
/>
|
|
<div className="min-w-0 flex-1">
|
|
<div className="flex items-baseline justify-between gap-2">
|
|
<span className="truncate text-sm font-medium text-foreground">{title}</span>
|
|
<span className="shrink-0 text-ui-label text-muted-foreground/80">
|
|
{formatRelativeTime(run.updatedAt)}
|
|
</span>
|
|
</div>
|
|
<div className="mt-0.5 flex items-baseline gap-2">
|
|
{run.promptSnippet ? (
|
|
<span className="min-w-0 flex-1 truncate text-ui-body-sm text-muted-foreground">
|
|
{run.promptSnippet}
|
|
</span>
|
|
) : null}
|
|
{isNeedsYouStatus(run.status) ? (
|
|
<span className="ms-auto shrink-0 rounded-full border border-warning/30 bg-warning/10 px-2 py-0.5 text-ui-label font-medium text-warning">
|
|
{label}
|
|
</span>
|
|
) : (
|
|
<span className={`ms-auto shrink-0 text-ui-label ${tone}`}>{label}</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</button>
|
|
);
|
|
}
|
|
|
|
function formatRelativeTime(iso: string, now = new Date()): string {
|
|
const date = new Date(iso);
|
|
if (Number.isNaN(date.getTime())) return "";
|
|
const seconds = Math.floor((now.getTime() - date.getTime()) / 1000);
|
|
if (seconds < 45) return t`just now`;
|
|
const minutes = Math.floor(seconds / 60);
|
|
if (minutes < 60) return t`${minutes}m ago`;
|
|
const hours = Math.floor(minutes / 60);
|
|
if (hours < 24) return t`${hours}h ago`;
|
|
const days = Math.floor(hours / 24);
|
|
if (days < 7) return t`${days}d ago`;
|
|
return date.toLocaleDateString(i18n.locale || "en", { month: "short", day: "numeric" });
|
|
}
|
|
|
|
function statusLabel(status: RunActivityRow["status"]): string {
|
|
if (isNeedsYouStatus(status)) return t`Needs you`;
|
|
return runActivityLabel(status);
|
|
}
|