// SOLARA SDR — view components.
// Adapted from the Stitch handoff. All data flows through props from real
// /api/* endpoints; no mock state. The visual structure mirrors the design
// pixel-for-pixel (see static/styles.css).
const fmt = {
num: (n) => (n == null ? "—" : Number(n).toLocaleString()),
pct: (n, d = 1) => (n == null ? "—" : `${Number(n).toFixed(d)}%`),
// epoch seconds → relative ("12s ago", "3m ago", "Mar 14")
rel: (ts) => {
if (!ts) return "—";
const now = Math.floor(Date.now() / 1000);
const d = now - Number(ts);
if (d < 0) return "now";
if (d < 60) return `${d}s ago`;
if (d < 3600) return `${Math.floor(d / 60)}m ago`;
if (d < 86400) return `${Math.floor(d / 3600)}h ago`;
if (d < 86400 * 7) return `${Math.floor(d / 86400)}d ago`;
const dt = new Date(ts * 1000);
return dt.toLocaleDateString("en-US", { month: "short", day: "numeric" });
},
shortName: (n) => {
if (!n) return "??";
const parts = String(n).split(/[\s,@.]+/).filter(Boolean);
return parts.slice(0, 2).map((s) => s[0].toUpperCase()).join("") || "??";
},
};
// =================================================================
// OVERVIEW
// =================================================================
function OverviewView({ data, onNav, onOpenAside }) {
const t = data.totals || {};
const sparkData = data.sparkline?.buckets || [];
return (
<>
Pipeline overview
{data.campaigns.filter((c) => c.enabled).length} campaigns enabled.
{" "}
{fmt.num(t.leads_inflight)} leads in flight · agent engine{" "}
{data.engine_status || "scaffold"}.
{/* KPI row */}
Leads
all-time
{fmt.num(t.suppressions || 0)} suppressed
Sent · 12h
live
{fmt.num(sparkData.reduce((a, b) => a + b, 0))}
{fmt.num(t.msgs_sent || 0)} all-time outbound
{sparkData.length > 0 && (
)}
Replies
{fmt.pct(t.reply_rate || 0)} reply rate
{fmt.num(t.msgs_received || 0)}
{fmt.num(t.leads_replied || 0)} leads replied
Open escalations
needs review
{data.open_escalations > 0 ? "Awaiting human" : "All clear"}
{/* Funnel */}
Pipeline · all time
{fmt.num(t.leads_total)} leads
→
{fmt.num(t.leads_converted || 0)} won
{data.funnel && data.funnel.length > 0 ? (
) : (
Funnel data unavailable.
)}
{/* Agent / engine status */}
Agent status
{data.engine_status === "operational" ? "OPERATING" : "SCAFFOLD"}
Mailboxes
{data.campaigns.length} configured · 0 polling
● IMAP listener pending (Day 2)
LLM engine
not yet wired
scheduler + sender Day 2
Database
SQLite · {fmt.num(t.leads_total)} leads
● healthy
Suppressions
{fmt.num(t.suppressions || 0)} addresses
bounce-webhook → suppression list active
{/* Top conversations */}
Top conversations
{data.threads && data.threads.length > 0 ? (
| Lead |
Campaign |
Subject |
Status |
When |
{data.threads.slice(0, 6).map((th) => {
const c = data.campaigns.find((c) => c.id === th.campaign_id) || {};
return (
onNav("inbox", th.id)}>
|
{th.lead.name}
{th.lead.company || th.lead.email}
|
{c.display_name || th.campaign_id}
|
{th.subject}
|
|
{fmt.rel(th.ts)}
|
);
})}
) : (
)}
{/* Campaign quick cards */}
Campaigns
{data.campaigns.slice(0, 6).map((c) => {
const pc = data.per_campaign?.[c.id] || {};
const total = pc.leads_total || 0;
const active = pc.leads_inflight || 0;
const pct = total > 0 ? active / total : 0;
const replyRate =
pc.leads_total > 0
? ((pc.leads_replied || 0) / pc.leads_total) * 100
: 0;
return (
onNav("campaigns", c.id)}
style={{
padding: "10px 12px",
border: "1px solid var(--border-soft)",
borderRadius: 10,
cursor: "pointer",
transition: "all 150ms var(--ease)",
}}
>
{c.display_name}
· {c.persona_name}
{c.enabled ? null : (
paused
)}
{fmt.num(active)} / {fmt.num(total)} active
{fmt.pct(replyRate)} reply
);
})}
{/* Escalation queue */}
Escalation queue · awaiting human
{data.escalations_queue?.length || 0} open
{data.escalations_queue && data.escalations_queue.length > 0 ? (
|
Lead |
Reason |
Campaign |
Age |
Actions |
{data.escalations_queue.map((e) => {
const c = data.campaigns.find((c) => c.id === e.campaign_id) || {};
return (
|
|
{e.lead}
{e.company || ""}
|
{e.reason} |
{c.display_name || e.campaign_id}
|
{fmt.rel(e.created_at)}
|
|
);
})}
) : (
)}
>
);
}
const kpiLabel = {
fontSize: 11,
textTransform: "uppercase",
letterSpacing: "0.06em",
fontWeight: 600,
marginBottom: 6,
};
const kpiData = { fontFamily: "var(--font-mono)", fontSize: 12 };
const kpiSub = {
fontFamily: "var(--font-mono)",
fontSize: 10.5,
color: "var(--fg-muted)",
marginTop: 2,
};
// =================================================================
// INBOX
// =================================================================
function InboxView({ data, threadId, setThreadId, onOpenRail }) {
const [search, setSearch] = useState("");
const [filter, setFilter] = useState("all"); // all | unread
const [pane, setPane] = useState("list"); // mobile single-pane router
const [thread, setThread] = useState(null);
const [loading, setLoading] = useState(false);
const threads = useMemo(() => {
let list = data.threads || [];
if (filter === "unread") list = list.filter((t) => t.unread);
if (search) {
const s = search.toLowerCase();
list = list.filter((t) =>
((t.lead.name || "") +
(t.lead.company || "") +
(t.subject || "") +
(t.snippet || "")
)
.toLowerCase()
.includes(s)
);
}
return list;
}, [data.threads, filter, search]);
// Auto-select first thread once data is available.
useEffect(() => {
if (!threadId && threads.length > 0) {
setThreadId(threads[0].id);
}
}, [threadId, threads, setThreadId]);
// Load thread detail.
useEffect(() => {
if (!threadId) {
setThread(null);
return;
}
const leadId = threadId.replace(/^l/, "");
setLoading(true);
fetch(`/api/threads/${leadId}`)
.then((r) => (r.ok ? r.json() : null))
.then((data) => {
setThread(data);
setLoading(false);
})
.catch(() => setLoading(false));
}, [threadId]);
const t = thread?.lead;
const c = data.campaigns.find((cc) => cc.id === t?.campaign_id) || {};
return (
<>
Inbox
{threads.length} conversation{threads.length === 1 ? "" : "s"} ·{" "}
{data.open_escalations} need attention
{/* Thread list */}
setSearch(e.target.value)}
/>
{threads.length === 0 ? (
) : (
threads.map((th) => {
const tc = data.campaigns.find((cc) => cc.id === th.campaign_id) || {};
return (
{
setThreadId(th.id);
setPane("view");
}}
>
{th.lead.name}
{fmt.rel(th.ts)}
{th.subject}
{th.snippet || ""}
{tc.display_name || th.campaign_id}
);
})
)}
{/* Thread view */}
{thread && t ? (
<>
{fmt.shortName(t.name)}
{t.name}
{t.title &&
{t.title}}
{t.title && t.company && (
·
)}
{t.company && (
{t.company}
)}
{t.email && (
<>
·
{t.email}
>
)}
{t.linkedin && (
<>
·
linkedin
>
)}
{thread.messages && thread.messages.length > 0 ? (
thread.messages.map((m) => (
{m.direction === "outbound" ? c.persona_name : t.name}
{m.direction === "outbound" ? m.from_addr : m.from_addr}
{fmt.rel(m.sent_at || m.received_at || m.created_at)}
{m.body_text || "(no body)"}
))
) : (
)}
AI draft
pending Day-2 LLM engine
Drafts will appear here once the LLM engine ships. The persona
({c.persona_name}) and ICP brief from{" "}
{c.display_name} are already wired in.
>
) : loading ? (
Loading thread…
) : (
)}
>
);
}
// =================================================================
// PIPELINE
// =================================================================
function PipelineView({ data, onNav }) {
const [pipeline, setPipeline] = useState(null);
const [campFilter, setCampFilter] = useState("all");
useEffect(() => {
const url =
campFilter === "all"
? "/api/pipeline"
: `/api/pipeline?campaign_id=${encodeURIComponent(campFilter)}`;
fetch(url)
.then((r) => r.json())
.then(setPipeline);
}, [campFilter]);
if (!pipeline) {
return (
Loading pipeline…
);
}
const total = Object.values(pipeline.by_stage).reduce(
(a, arr) => a + arr.length,
0
);
return (
<>
Pipeline
{total} lead{total === 1 ? "" : "s"} in motion · stage updates from
agent activity
{data.campaigns.map((c) => (
))}
{pipeline.stages.map((stage) => {
const items = pipeline.by_stage[stage.id] || [];
return (
{stage.name}
{items.length}
{items.length === 0 ? (
Empty
) : (
items.map((l) => {
const c = data.campaigns.find((c) => c.id === l.campaign_id) || {};
return (
onNav("inbox", l.id)}
>
{l.title || ""}
{l.title && l.company ? " · " : ""}
{l.company || ""}
{c.display_name || l.campaign_id}
{l.email}
);
})
)}
);
})}
>
);
}
// =================================================================
// CAMPAIGNS
// =================================================================
function CampaignsView({ data, onUpload }) {
return (
<>
Campaigns
{data.campaigns.filter((c) => c.enabled).length} enabled · drop a
CSV onto any card to add leads
{data.campaigns.map((c) => {
const pc = data.per_campaign?.[c.id] || {};
const total = pc.leads_total || 0;
const inflight = pc.leads_inflight || 0;
const replied = pc.leads_replied || 0;
const converted = pc.leads_converted || 0;
const replyRate = total > 0 ? (replied / total) * 100 : 0;
const pct = total > 0 ? inflight / total : 0;
return (
);
})}
>
);
}
function CampaignCard({
campaign: c,
total,
inflight,
replied,
converted,
replyRate,
pct,
onUpload,
}) {
const [drag, setDrag] = useState(false);
const [result, setResult] = useState(null); // {kind, msg}
const inputRef = useRef(null);
const upload = (file) => {
if (!file) return;
setResult({ kind: "work", msg: `Uploading ${file.name}…` });
const fd = new FormData();
fd.append("file", file);
fetch(`/api/campaigns/${encodeURIComponent(c.id)}/leads/csv`, {
method: "POST",
body: fd,
})
.then((r) => {
if (!r.ok) throw new Error("HTTP " + r.status);
return r.json();
})
.then((d) => {
setResult({
kind: "ok",
msg: `Imported ${d.inserted} · ${d.skipped} skipped`,
});
if (onUpload) setTimeout(onUpload, 600);
})
.catch((e) => setResult({ kind: "err", msg: "Failed: " + e.message }));
};
return (
{
e.preventDefault();
setDrag(true);
}}
onDragOver={(e) => {
e.preventDefault();
setDrag(true);
}}
onDragLeave={() => setDrag(false)}
onDrop={(e) => {
e.preventDefault();
setDrag(false);
const f = e.dataTransfer?.files?.[0];
if (f) upload(f);
}}
>
{c.display_name}
{c.persona_name} · {c.sender_email}
{c.enabled ? "Enabled" : "Paused"}
{c.vertical || ""}
{fmt.num(inflight)}
Active
{fmt.pct(replyRate)}
Reply
{Math.round(pct * 100)}% active
{fmt.num(total - inflight)} remaining
inputRef.current?.click()}
style={{
marginTop: 14,
padding: 10,
border: `1.5px dashed ${
drag ? "var(--primary)" : "var(--border)"
}`,
borderRadius: 8,
textAlign: "center",
background: drag ? "hsl(var(--hsl-primary) / 0.06)" : "var(--bg)",
color: drag ? "var(--primary)" : "var(--fg-muted)",
cursor: "pointer",
fontSize: 12,
transition: "all 120ms var(--ease)",
}}
>
{
const f = e.target.files?.[0];
if (f) upload(f);
e.target.value = "";
}}
/>
Drop CSV or click ·
needs an{" "}
email column
{result && (
{result.msg}
)}
);
}
// =================================================================
// EMPTY STATE
// =================================================================
function EmptyState({ icon, title, text }) {
return (
);
}
Object.assign(window, {
OverviewView,
InboxView,
PipelineView,
CampaignsView,
EmptyState,
fmt,
});