feat(confidence-engine): v0.59b-c — report freshness on Report page + Portfolio
v0.59b — Report page freshness UI: - Shows Current / Update available beside the generated report - Manual Update report action with duplicate prevention guard - Explanation copy about investigation changes since generation - Persists generatedFromRevision during update flow v0.59c — Portfolio Report freshness state: - Surfaces Current / Update available alongside existing View report link - Derives solely from revision provenance (zero model calls) - No Update report action on Portfolio (manual update owned by Report page) - Neither state shown when no Report exists - Updated makeSnapshot with investigationRevision for realistic test data
This commit is contained in:
@@ -9,6 +9,7 @@ export default function ReportPage() {
|
||||
const [hydrated, setHydrated] = useState(false);
|
||||
const [generationLoading, setGenerationLoading] = useState(false);
|
||||
const [generationError, setGenerationError] = useState(false);
|
||||
const [updateLoading, setUpdateLoading] = useState(false);
|
||||
|
||||
const generationAttempted = useRef(false);
|
||||
|
||||
@@ -66,6 +67,48 @@ export default function ReportPage() {
|
||||
})();
|
||||
}, [hydrated, existing]);
|
||||
|
||||
// Manual Report update (v0.59b — freshness manual update)
|
||||
const handleUpdateReport = async () => {
|
||||
if (updateLoading) return;
|
||||
setUpdateLoading(true);
|
||||
|
||||
const snap = loadInvestigation();
|
||||
const situationGraph = snap?.situationGraph;
|
||||
const findings = snap?.findings ?? [];
|
||||
const rev = snap?.investigationRevision ?? 0;
|
||||
|
||||
if (!situationGraph) {
|
||||
setUpdateLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/cases/overview", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ situationGraph, findings }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
setUpdateLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
const reportData = { understanding: data.understanding, plausibleInterpretations: data.plausibleInterpretations, hasPlausibleInterpretations: true, generatedFromRevision: rev };
|
||||
setExisting((p) => {
|
||||
saveInvestigation({ ...p, investigationReport: reportData });
|
||||
return { ...p, investigationReport: reportData };
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
/* failure: retain existing Report and updateAvailable state */
|
||||
} finally {
|
||||
setUpdateLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const report = existing?.investigationReport || null;
|
||||
const scenario = hydrated ? (existing?.scenario || "") : null;
|
||||
|
||||
@@ -79,6 +122,28 @@ export default function ReportPage() {
|
||||
Investigation Report
|
||||
</h1>
|
||||
|
||||
{/* Report freshness — only when a Report exists */}
|
||||
{report ? (
|
||||
<div className="mt-6 flex items-center gap-3">
|
||||
{existing?.investigationRevision === report.generatedFromRevision ? (
|
||||
<span className="text-[11px] font-semibold tracking-wider uppercase text-teal-700/70">Current</span>
|
||||
) : (
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-[11px] font-semibold tracking-wider uppercase text-gray-500">Update available</span>
|
||||
<span className="text-xs text-gray-400">The investigation has changed since this report was generated.</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleUpdateReport}
|
||||
disabled={updateLoading}
|
||||
className="rounded-lg border border-teal-600 bg-white px-3 py-1.5 text-[11px] font-semibold tracking-wider uppercase text-teal-700 hover:bg-teal-50 transition disabled:opacity-40"
|
||||
>
|
||||
{updateLoading ? "Updating…" : "Update report"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Situation */}
|
||||
{scenario && (
|
||||
<div className="mt-8 rounded-xl border-[2.5px] border-teal-300/70 bg-gradient-to-b from-teal-50/60 to-white px-8 pt-6 pb-7 shadow-sm">
|
||||
|
||||
+23
-6
@@ -18,6 +18,11 @@ function Portfolio() {
|
||||
existing?.investigationReport && existing.investigationReport.understanding
|
||||
);
|
||||
|
||||
const reportIsCurrent =
|
||||
hasReport &&
|
||||
existing.investigationReport.generatedFromRevision ===
|
||||
(existing.investigationRevision ?? 0);
|
||||
|
||||
return (
|
||||
<main className="mx-auto max-w-[640px] px-6 py-16">
|
||||
<h1 className="mb-2 text-3xl font-bold tracking-tight">Confidence Engine</h1>
|
||||
@@ -39,12 +44,24 @@ function Portfolio() {
|
||||
|
||||
<div className="mt-4 flex gap-3 text-sm">
|
||||
{hasReport ? (
|
||||
<Link
|
||||
href={`/investigations/${INVESTIGATION_ID}/report`}
|
||||
className="rounded-lg border border-teal-600 bg-white px-4 py-2 font-medium text-teal-700 hover:bg-teal-50 transition"
|
||||
>
|
||||
View report
|
||||
</Link>
|
||||
<div className="flex items-center gap-2">
|
||||
<Link
|
||||
href={`/investigations/${INVESTIGATION_ID}/report`}
|
||||
className="rounded-lg border border-teal-600 bg-white px-4 py-2 font-medium text-teal-700 hover:bg-teal-50 transition"
|
||||
>
|
||||
View report
|
||||
</Link>
|
||||
|
||||
{reportIsCurrent ? (
|
||||
<span className="text-[11px] font-semibold tracking-wider uppercase text-teal-700/70">
|
||||
Current
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-[11px] font-semibold tracking-wider uppercase text-gray-500">
|
||||
Update available
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<Link
|
||||
|
||||
Reference in New Issue
Block a user