File size: 5,288 Bytes
f65a6b3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
// EvidenceReport β€” a clean, printable one-page summary of an answer and the
// evidence behind it. Rendered off-screen until the user triggers a print; we
// flip a class on <html> so a print-only stylesheet shows just the report.
// No new deps β€” window.print() does the work.
import { useEffect } from "react";
import { highlightMatch } from "../lib/highlight";

function fmtConfidence(c) {
  if (typeof c !== "number") return "β€”";
  return `${Math.round(c * 100)}%`;
}

// One printable evidence row. Mirrors EvidencePanel's metadata line + excerpt,
// but always expanded (a report shows everything) and ink-on-paper styled.
function ReportEvidence({ citation, rank }) {
  const meta = [
    citation.page != null ? `page ${citation.page}` : null,
    `chunk #${citation.chunk_index}`,
    citation.score != null ? `relevance ${citation.score}` : null,
  ]
    .filter(Boolean)
    .join(" Β· ");

  return (
    <div className="report-evidence">
      <div className="report-evidence-head">
        <span className="report-rank">{rank}</span>
        <span className="report-doc">{citation.document_name}</span>
        <span className="report-meta">{meta}</span>
      </div>
      <div className="report-excerpt">
        {highlightMatch(citation.excerpt, citation.match_text)}
      </div>
    </div>
  );
}

export default function EvidenceReport({ result, question, onClose }) {
  // Trigger the browser print dialog once mounted, then notify the parent so it
  // can drop the report from the tree. afterprint covers both "Print" and
  // "Cancel" so we never get stuck in the print-only view.
  useEffect(() => {
    if (!result) return;
    const root = document.documentElement;
    root.classList.add("printing-report");
    let finished = false;
    let fallback;
    // Runs exactly once: drop the print-only class and notify the parent so it
    // can unmount the report. Must ALWAYS fire (even if printing fails), or the
    // parent is left with a stuck report and a later re-export can't reopen.
    const done = () => {
      if (finished) return;
      finished = true;
      clearTimeout(fallback);
      window.removeEventListener("afterprint", done);
      root.classList.remove("printing-report");
      onClose?.();
    };
    window.addEventListener("afterprint", done);
    // Let the report paint before opening the dialog.
    const t = setTimeout(() => {
      try {
        window.print();
      } catch {
        done(); // print blocked/threw β€” don't leave the report stuck
        return;
      }
      // Safety net for environments where `afterprint` never fires (sandboxed
      // iframes / print-blocking browsers): clean up anyway.
      fallback = setTimeout(done, 3000);
    }, 60);
    return () => {
      clearTimeout(t);
      clearTimeout(fallback);
      window.removeEventListener("afterprint", done);
      root.classList.remove("printing-report");
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  if (!result) return null;
  const citations = result.citations || [];
  const meta = result.metadata || {};
  const supported = meta.evidence_status === "supported" && !result.abstained;

  return (
    <div className="evidence-report" role="document" aria-label="Evidence report">
      <header className="report-header">
        <div>
          <div className="report-brand">FinanceFlow AI Β· Evidence Report</div>
          <div className="report-date">{new Date().toLocaleString()}</div>
        </div>
        <div className={`report-status ${supported ? "ok" : "weak"}`}>
          {supported ? "Grounded" : "Not enough info"}
        </div>
      </header>

      <section className="report-block">
        <h2 className="report-label">Question</h2>
        <p className="report-question">{question}</p>
      </section>

      <section className="report-block">
        <div className="report-answer-head">
          <h2 className="report-label">Answer</h2>
          <div className="report-conf">
            <span className="report-conf-num">{fmtConfidence(meta.confidence)}</span>
            <span className="report-conf-lbl">confidence</span>
          </div>
        </div>
        <p className="report-answer">{result.answer}</p>
        <div className="report-facts">
          <span>Mode: {result.mode || "β€”"}</span>
          <span>Sources cited: {citations.length}</span>
          {meta.question_type && <span>Type: {meta.question_type}</span>}
          {typeof meta.top_score === "number" && (
            <span>Top match: {meta.top_score}</span>
          )}
        </div>
      </section>

      <section className="report-block">
        <h2 className="report-label">
          Evidence{citations.length > 0 ? ` (${citations.length})` : ""}
        </h2>
        {citations.length > 0 ? (
          citations.map((c, i) => (
            <ReportEvidence key={i} citation={c} rank={i + 1} />
          ))
        ) : (
          <p className="report-noevidence">
            No supporting passages β€” the assistant abstained rather than guess.
          </p>
        )}
      </section>

      <footer className="report-footer">
        Generated by FinanceFlow AI β€” answers are grounded in the cited source
        passages above.
      </footer>
    </div>
  );
}