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();
}