diff --git a/app/(marketing)/recent/outreach-form.tsx b/app/(marketing)/recent/outreach-form.tsx index 8565e572..6b2b0f33 100644 --- a/app/(marketing)/recent/outreach-form.tsx +++ b/app/(marketing)/recent/outreach-form.tsx @@ -2,7 +2,7 @@ import Link from "next/link"; import { useRouter } from "next/navigation"; -import { FormEvent, useMemo, useState, useTransition } from "react"; +import { FormEvent, useEffect, useMemo, useState, useTransition } from "react"; import { retryRecentOutreach, sendRecentAuditOutreach, @@ -42,6 +42,7 @@ export function RecentOutreachForm({ creditsBalance: number; history: OutreachHistoryItem[]; }) { + const router = useRouter(); const initialChannel = hasEmail ? "email" : hasPhone ? "sms" : "social"; const [channel, setChannel] = useState<"email" | "sms" | "social">(initialChannel); const [visibility, setVisibility] = useState<"private" | "public">("private"); @@ -67,6 +68,19 @@ export function RecentOutreachForm({ ); const insufficient = cost > 0 && creditsBalance < cost; + // Progress across this audit's outreach history. Cookie/browser posts move + // asynchronously (queued → awaiting_code/publishing → sent/failed) as the + // worker churns through them, so summarize where they stand. + const progress = useMemo(() => summarizeHistory(history), [history]); + + // Poll while anything is still in flight so rows update in real time without + // a manual reload; stop once everything is terminal to avoid idle load. + useEffect(() => { + if (!progress.inFlight) return; + const id = setInterval(() => router.refresh(), OUTREACH_REFRESH_MS); + return () => clearInterval(id); + }, [progress.inFlight, router]); + function submit(event: FormEvent) { event.preventDefault(); setMessage(null); @@ -115,7 +129,18 @@ export function RecentOutreachForm({ {history.length > 0 && (
-
History
+
+ History + {progress.inFlight ? ( + + + Live · updating… + + ) : ( + Idle + )} +
+ {history.map((h) => (
{statusLabel(h.status)} @@ -331,6 +356,51 @@ function RetryOutreachButton({ messageId }: { messageId: string }) { ); } +const OUTREACH_REFRESH_MS = 8000; + +type OutreachProgress = { + total: number; + sent: number; + failed: number; + pending: number; + inFlight: boolean; +}; + +// A post is "pending" (still moving) while queued or awaiting a code; "failed" +// covers failed + timed out. inFlight drives the live-refresh loop. +function summarizeHistory(history: OutreachHistoryItem[]): OutreachProgress { + let sent = 0; + let failed = 0; + let pending = 0; + for (const h of history) { + if (h.status === "sent") sent++; + else if (h.status === "failed" || h.status === "timed_out") failed++; + else pending++; // queued | awaiting_code + } + return { total: history.length, sent, failed, pending, inFlight: pending > 0 }; +} + +function OutreachProgressBar({ progress }: { progress: OutreachProgress }) { + const { total, sent, failed, pending } = progress; + if (total === 0) return null; + const pct = (n: number) => `${(n / total) * 100}%`; + return ( +
+
+
+
+
+
+
+ {sent} sent · {failed} failed · {pending} pending + + ({sent + failed}/{total} done) + +
+
+ ); +} + function defaultBody(host: string) { return `I saw your CrawlProof audit for ${host}. There are a few concrete fixes that would improve how AI engines understand the site.`; }