Spaces:
Sleeping
Sleeping
File size: 5,031 Bytes
935d7f2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | /**
* ActiveUsersTable β admin-facing customer outreach view.
*
* Each row is a user active in the selected window:
* user_id_hash Β· top_topic Β· interest_score Β· last_activity Β· contact-ready badge
*
* Clicking "Inspect" sets the user as the inspected user on the page, jumping
* to their cognitive-facet cards above.
*/
export default function ActiveUsersTable({ rows, onInspect, windowLabel = "", activeHash = "" }) {
if (!rows || rows.length === 0) {
return (
<div className="empty-state">
No users active in window <strong>{windowLabel || "β"}</strong>.
</div>
);
}
return (
<table className="tbl active-users-tbl">
<thead>
<tr>
<th>User</th>
<th>Top topic</th>
<th className="num">Interest</th>
<th className="num">Turns</th>
<th className="num">Topics</th>
<th className="num">Pos / total rewards</th>
<th>Last seen</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
{rows.map((u) => {
const isActive = activeHash === u.user_id_hash;
const rowClass = [
u.contact_ready ? "ready" : "",
isActive ? "row-active" : "",
].filter(Boolean).join(" ");
return (
<tr key={u.user_id_hash} className={rowClass}>
<td>
<div className="user-name">{u.display_name || shortHash(u.user_id_hash)}</div>
<code className="user-hash" title="Persisted as a SHA-256 hash; this is the only identifier stored in the analytics layer.">
{shortHash(u.user_id_hash)}
</code>
{u.intents?.length > 0 && (
<div className="intent-mini">
{u.intents.slice(0, 3).map((i) => (
<span key={i} className="intent-mini-pill">{i}</span>
))}
</div>
)}
</td>
<td>
<span className="topic-chip">{u.top_topic || "β"}</span>
{u.topics?.length > 1 && (
<span className="topic-more">+{u.topics.length - 1} more</span>
)}
</td>
<td className={`num ${interestClass(u.top_interest_score)}`}>
<strong>{fmt(u.top_interest_score)}</strong>
</td>
<td className="num">{u.turn_count}</td>
<td className="num">{u.topic_count}</td>
<td className="num">
{u.positive_rewards}
<span className="reward-divider"> / </span>
{u.applied_rewards}
</td>
<td className="last-seen">{relativeTime(u.last_activity_ts)}</td>
<td>
{u.do_not_contact ? (
<span className="status-blocked" title="do_not_contact flag is set in the directory">
β Do not contact
</span>
) : !u.compliance_eligible ? (
<span className="status-blocked" title="compliance_eligible=false in the directory">
β Compliance block
</span>
) : u.contact_ready ? (
<span className="contact-ready-yes" title={u.recommendation_reason}>
β Contact ready
</span>
) : (
<span className="contact-ready-no" title={u.recommendation_reason}>
β Below threshold
</span>
)}
{u.recommendation_reason && (
<div className="recommendation-reason">{u.recommendation_reason}</div>
)}
</td>
<td>
<button
className={`btn-secondary btn-tiny ${isActive ? "btn-active" : ""}`}
onClick={() => onInspect?.(u.user_id_hash)}
title="Load this user's cognitive facets, interests, and offers"
disabled={isActive}
>
{isActive ? "β Inspecting" : "Inspect β"}
</button>
</td>
</tr>
);
})}
</tbody>
</table>
);
}
function shortHash(h) {
if (!h) return "β";
return h.length > 10 ? `${h.slice(0, 10)}β¦` : h;
}
function interestClass(v) {
const x = Number(v) || 0;
if (x >= 0.7) return "pos";
if (x <= 0.3) return "zero";
return "";
}
function fmt(v) {
if (v == null) return "β";
const x = Number(v);
if (!Number.isFinite(x)) return "β";
return x.toFixed(2);
}
function relativeTime(iso) {
if (!iso) return "β";
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return iso;
const diffMs = Date.now() - d.getTime();
const m = Math.round(diffMs / 60000);
if (m < 1) return "just now";
if (m < 60) return `${m}m ago`;
const h = Math.round(m / 60);
if (h < 24) return `${h}h ago`;
const days = Math.round(h / 24);
if (days < 30) return `${days}d ago`;
return d.toLocaleDateString();
}
|