File size: 9,758 Bytes
baf8a7b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
import { useEffect, useRef, useState } from "react";
import { Stack, ArrowRight, X } from "@phosphor-icons/react";
import { api, factValue, type Fact, type RunSummary } from "../lib/api";
import { Empty, Note, ProvenanceList, Spinner, StatusChip } from "./common";

const FILTERS = [
  { k: "", label: "all" },
  { k: "verified", label: "verified" },
  { k: "derived", label: "derived" },
  { k: "unverified", label: "unverified" },
  { k: "conflicted", label: "conflicted" },
  { k: "attested", label: "attested" },
  { k: "resolved", label: "resolved" },
] as const;

export function Ledger({ run, onNext }: { run: RunSummary; onNext: () => void }) {
  const [facts, setFacts] = useState<Fact[]>([]);
  const [total, setTotal] = useState(run.fact_count);
  const [streaming, setStreaming] = useState(true);
  const [filter, setFilter] = useState<string>("");
  const [open, setOpen] = useState<Fact | null>(null);
  const seen = useRef(new Set<string>());

  useEffect(() => {
    // Stream the ledger so the reviewer watches it being assembled from the filing.
    const es = new EventSource(`/api/runs/${run.id}/ledger/stream?delay_ms=14`);
    es.addEventListener("start", (e) => setTotal(JSON.parse((e as MessageEvent).data).total));
    es.addEventListener("fact", (e) => {
      const { fact } = JSON.parse((e as MessageEvent).data);
      if (seen.current.has(fact.id)) return;
      seen.current.add(fact.id);
      setFacts((prev) => [...prev, fact]);
    });
    es.addEventListener("done", () => { setStreaming(false); es.close(); });
    es.onerror = () => { setStreaming(false); es.close(); };
    return () => es.close();
  }, [run.id]);

  const shown = filter ? facts.filter((f) => f.status === filter) : facts;
  const counts = facts.reduce<Record<string, number>>((a, f) => {
    a[f.status] = (a[f.status] ?? 0) + 1;
    return a;
  }, {});

  return (
    <>
      <div className="page-head">
        <h1 className="page">Disclosure ledger</h1>
        <p className="lede">
          Each row is one atomic disclosure value read from the issuer's own filing, bound to the
          page it came from. Status is earned, never assumed: a figure is verified only when two
          independent sections agree or an accounting identity supports it. Everything else
          abstains and says what to confirm.
        </p>
      </div>

      <div className="stats" style={{ marginBottom: 14 }}>
        <div className="stat">
          <div className="stat-v">{facts.length}</div>
          <div className="stat-l">facts extracted{streaming ? " so far" : ""}</div>
        </div>
        <div className="stat">
          <div className="stat-v ok">{counts.verified ?? 0}</div>
          <div className="stat-l">verified by corroboration or identity</div>
        </div>
        <div className="stat">
          <div className="stat-v">{counts.derived ?? 0}</div>
          <div className="stat-l">derived, with lineage recorded</div>
        </div>
        <div className="stat">
          <div className="stat-v warn">{counts.unverified ?? 0}</div>
          <div className="stat-l">abstained, awaiting confirmation</div>
        </div>
        <div className="stat">
          <div className="stat-v stop">{counts.conflicted ?? 0}</div>
          <div className="stat-l">sources disagree</div>
        </div>
      </div>

      {streaming && (
        <div className="bar" style={{ marginBottom: 14 }}>
          <i style={{ width: `${total ? (facts.length / total) * 100 : 0}%` }} />
        </div>
      )}

      <section className="panel">
        <div className="panel-h">
          <Stack size={16} />
          <h2>Facts</h2>
          <span className="count">{shown.length} shown</span>
          <div className="filters" style={{ marginLeft: "auto" }}>
            {FILTERS.map((f) => (
              <button
                key={f.k}
                className="filter"
                aria-pressed={filter === f.k}
                onClick={() => setFilter(f.k)}
              >
                {f.label}
                {f.k && counts[f.k] ? ` ${counts[f.k]}` : ""}
              </button>
            ))}
          </div>
        </div>

        <div style={{ maxHeight: "58vh", overflowY: "auto" }}>
          {shown.length === 0 && !streaming ? (
            <Empty title={filter ? `No ${filter} facts` : "The ledger is empty"}>
              {filter
                ? "No figure in this filing earned that status."
                : "No disclosure value could be read from the source records."}
            </Empty>
          ) : (
            <table className="tbl">
              <thead>
                <tr>
                  <th style={{ width: "34%" }}>Disclosure</th>
                  <th className="r" style={{ width: "16%" }}>Value</th>
                  <th style={{ width: "12%" }}>Status</th>
                  <th>Source</th>
                </tr>
              </thead>
              <tbody>
                {shown.map((f, i) => (
                  <tr
                    key={f.id}
                    className={i >= shown.length - 3 && streaming ? "arrived" : ""}
                    onClick={() => setOpen(f)}
                    style={{ cursor: "pointer" }}
                  >
                    <td>
                      <div className="f-label">{f.label}</div>
                      <div className="f-id">{f.id}</div>
                      {f.note && <div className="f-note">{f.note}</div>}
                    </td>
                    <td className="r num" style={{ fontWeight: 500 }}>{factValue(f)}</td>
                    <td><StatusChip status={f.status} /></td>
                    <td>
                      <div className="f-src">{f.provenance[0]?.locator ?? "n/a"}</div>
                      {f.provenance.length > 1 && (
                        <div className="tag tag-ok" style={{ marginTop: 3, display: "inline-block" }}>
                          {f.provenance.length} sources agree
                        </div>
                      )}
                      {f.identities.length > 0 && (
                        <div className="f-id" style={{ marginTop: 3 }}>{f.identities[0]}</div>
                      )}
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          )}
        </div>
      </section>

      <div style={{ display: "flex", gap: 10, marginTop: 16, alignItems: "center" }}>
        <button className="btn btn-primary" onClick={onNext} disabled={streaming}>
          {streaming ? <><Spinner /> Reading filing</> : <>Draft the offer document <ArrowRight size={15} /></>}
        </button>
        {!streaming && (counts.unverified ?? 0) > 0 && (
          <span style={{ fontSize: 12.5, color: "var(--text-2)" }}>
            {counts.unverified} figures abstained. The engine will list each one for attestation.
          </span>
        )}
      </div>

      {open && <FactDrawer fact={open} onClose={() => setOpen(null)} />}
    </>
  );
}

function FactDrawer({ fact, onClose }: { fact: Fact; onClose: () => void }) {
  useEffect(() => {
    const onKey = (e: KeyboardEvent) => e.key === "Escape" && onClose();
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose]);

  return (
    <div
      style={{
        position: "fixed", inset: 0, background: "rgba(11,27,43,0.4)",
        zIndex: 50, display: "grid", placeItems: "center", padding: 20,
      }}
      onClick={onClose}
    >
      <div
        className="panel"
        style={{ maxWidth: 620, width: "100%", maxHeight: "82vh", overflowY: "auto" }}
        onClick={(e) => e.stopPropagation()}
        role="dialog"
        aria-label={fact.label}
      >
        <div className="panel-h">
          <h2>{fact.label}</h2>
          <button className="btn btn-ghost btn-sm" style={{ marginLeft: "auto" }} onClick={onClose}>
            <X size={13} /> Close
          </button>
        </div>
        <div className="panel-b" style={{ display: "grid", gap: 14 }}>
          <div style={{ display: "flex", gap: 12, alignItems: "baseline" }}>
            <span className="num" style={{ fontSize: 22, fontWeight: 600 }}>{factValue(fact)}</span>
            <StatusChip status={fact.status} />
            <span className="f-id">{fact.id}</span>
          </div>

          {fact.note && <Note tone="warn">{fact.note}</Note>}

          {fact.identities.length > 0 && (
            <div>
              <div className="f-src" style={{ marginBottom: 5 }}>Supported by</div>
              {fact.identities.map((i) => (
                <div key={i} className="mono" style={{ fontSize: 11.5, color: "var(--ok)" }}>{i}</div>
              ))}
            </div>
          )}

          {fact.derived_from.length > 0 && (
            <div>
              <div className="f-src" style={{ marginBottom: 5 }}>Derived from</div>
              {fact.derived_from.map((d) => (
                <div key={d} className="f-id">{d}</div>
              ))}
            </div>
          )}

          <div>
            <div className="f-src" style={{ marginBottom: 6 }}>
              Provenance ({fact.provenance.length})
            </div>
            <ProvenanceList provenance={fact.provenance} />
          </div>

          {fact.section_refs.length > 0 && (
            <div>
              <div className="f-src" style={{ marginBottom: 5 }}>Used in sections</div>
              <div style={{ display: "flex", gap: 5, flexWrap: "wrap" }}>
                {fact.section_refs.map((s) => (
                  <span key={s} className="tag">{s.replace(/_/g, " ")}</span>
                ))}
              </div>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}