Spaces:
Sleeping
Sleeping
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>
);
}
|