feat(confidence-engine): v0.54b integrate Investigation Overview UI seam + bounded scroll cleanup
- Wire transient overview state from ScenarioForm to ReasoningWorkspace - Inline rendering of investigation overview below milestone invitation - Remove obsolete scrollIntoView after overview request (scrolled away from rendered content) - All four overview props consumed in ReasoningWorkspace render path - Targeted Vitest: 9/9 PASS (tests/ui/investigation-overview-ui.test.jsx) - Production build: compiles successfully
This commit is contained in:
@@ -1331,6 +1331,11 @@ export default function ReasoningWorkspace({
|
||||
onSituationGraphChange,
|
||||
/* ── test init seam (no effect → immediate state) ───────── */
|
||||
initialPostAnalyseStatus,
|
||||
/* ── v0.54b — investigation overview transient state ─── */
|
||||
overviewState,
|
||||
setOverviewState,
|
||||
overviewLoading,
|
||||
handleRequestOverview,
|
||||
}) {
|
||||
const [investigationHistory, setInvestigationHistory] = useState([]);
|
||||
const turnCounter = useRef(0);
|
||||
@@ -1943,21 +1948,44 @@ export default function ReasoningWorkspace({
|
||||
})()}
|
||||
</div>
|
||||
) : openUnknowns.length === 0 && clarifiedQuestions.length > 0 && !cuSynthesisLoading ? (
|
||||
<div className="space-y-4">
|
||||
{/* Milestone invitation + overview action */}
|
||||
<div className="rounded-xl border-[2.5px] border-teal-300/60 bg-gradient-to-b from-teal-50/40 to-white px-7 pt-5 pb-6">
|
||||
<p className="text-sm leading-relaxed text-gray-700 mb-4">
|
||||
{'You\'ve now worked through all of the questions we surfaced. Would you like to see an overview of what we understand so far?'}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
const el = document.getElementById("cu-scroll-target");
|
||||
if (el) el.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
handleRequestOverview();
|
||||
}}
|
||||
style={{ cursor: "pointer" }}
|
||||
className="rounded-lg border border-teal-600 bg-white px-4 py-2 text-sm font-medium text-teal-700 hover:bg-teal-50 transition"
|
||||
disabled={overviewLoading}
|
||||
style={{ cursor: overviewLoading ? "wait" : "pointer" }}
|
||||
className="rounded-lg border border-teal-600 bg-white px-4 py-2 text-sm font-medium text-teal-700 hover:bg-teal-50 transition disabled:opacity-60"
|
||||
>
|
||||
Review current understanding
|
||||
{overviewLoading ? "Generating overview…" : "Review current understanding"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Investigation overview surface — transient, non-persistent */}
|
||||
{overviewState && (
|
||||
<div className="space-y-4" data-testid="investigation-overview">
|
||||
<div className="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">
|
||||
<h3 className="mb-3 text-[11px] font-bold tracking-[.18em] uppercase text-teal-700/70">
|
||||
What we understand
|
||||
</h3>
|
||||
<p className="text-base leading-relaxed text-gray-800">{overviewState.understanding}</p>
|
||||
</div>
|
||||
{overviewState.plausibleInterpretations && (
|
||||
<div className="rounded-xl border border-blue-200/70 bg-blue-50/40 px-8 pt-6 pb-7 shadow-sm">
|
||||
<h3 className="mb-3 text-[11px] font-bold tracking-[.18em] uppercase text-blue-600/70">
|
||||
What remains plausible
|
||||
</h3>
|
||||
<p className="text-sm leading-relaxed text-gray-700 italic">{overviewState.plausibleInterpretations}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* QUESTIONS WE HAVE CLARIFIED — resolved unknowns shown post-Done */}
|
||||
|
||||
@@ -285,6 +285,10 @@ export default function ScenarioForm() {
|
||||
const [mockScenario, setMockScenario] = useState("");
|
||||
const [hideFacilitatorOnLanding, setHideFacilitatorOnLanding] = useState(false);
|
||||
|
||||
/* ── v0.54b — investigation overview transient state ────── */
|
||||
const [overviewState, setOverviewState] = useState(null);
|
||||
const [overviewLoading, setOverviewLoading] = useState(false);
|
||||
|
||||
/* ── in-flight gate for episode reconsideration on Done ──── */
|
||||
const doneInProgressRef = useRef(false);
|
||||
|
||||
@@ -413,6 +417,36 @@ export default function ScenarioForm() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* v0.54b — request investigation overview via the established POST /api/cases/overview seam.
|
||||
* Transient state only: never persists to SituationGraph or currentUnderstanding.
|
||||
*/
|
||||
async function handleRequestOverview() {
|
||||
if (overviewLoading || !result?.situationGraph) return;
|
||||
|
||||
setOverviewLoading(true);
|
||||
setOverviewState(null); // clear any previous overview before new request
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/cases/overview", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
situationGraph: result.situationGraph,
|
||||
findings,
|
||||
plausibleInterpretations: (result.situationGraph?.reconstruction || {}).plausibleInterpretations ?? [],
|
||||
}),
|
||||
}).then((r) => r.json());
|
||||
|
||||
if (res?.success && res?.understanding != null) {
|
||||
setOverviewState(res);
|
||||
}
|
||||
// On failure: do not clear existing CU, do not block further attempts
|
||||
} finally {
|
||||
setOverviewLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function appendFocusedContribution(contribution) {
|
||||
// Derive a single stored contribution object and use it for BOTH
|
||||
// contribution storage AND Finding derivation so the same identity
|
||||
@@ -813,6 +847,11 @@ export default function ScenarioForm() {
|
||||
updateStatus={updateStatus}
|
||||
cuSynthesisLoading={cuSynthesisLoading}
|
||||
currentUnderstanding={currentUnderstanding}
|
||||
/* ── v0.54b — investigation overview transient state ─── */
|
||||
overviewState={overviewState}
|
||||
setOverviewState={setOverviewState}
|
||||
overviewLoading={overviewLoading}
|
||||
handleRequestOverview={handleRequestOverview}
|
||||
result={{
|
||||
...(result || {}),
|
||||
situationGraph: updateResult?.updatedSituationGraph ?? result?.situationGraph,
|
||||
|
||||
@@ -380,6 +380,45 @@ A fresh unanswered Question B displayed stale focused-investigation content from
|
||||
|
||||
> v0.54 overview synthesis apparatus established; no live semantic experiment and no UI integration performed yet.
|
||||
|
||||
### v0.54b — Bounded Investigation Overview UI integration (verified)
|
||||
|
||||
**Objective:** Answer whether a user-triggered Investigation Overview provides a useful semantic overview beyond existing Current Understanding, using inline transient rendering rather than scroll-to-CU.
|
||||
|
||||
**Bounded cleanup applied:**
|
||||
- Removed obsolete `scrollIntoView({ behavior: "smooth" })` call from milestone button onClick handler (line ~1958 of `reasoning-workspace.jsx`). The old behaviour scrolled away from the button to `cu-scroll-target` after starting `handleRequestOverview()`. Since the overview renders inline below this button, that scroll defeated the UX — the user clicked and the viewport moved elsewhere. Only the two `scrollIntoView` lines were removed; `handleRequestOverview()` call retained unchanged.
|
||||
- Unnecessary prop plumbing check: all four props (`overviewState`, `setOverviewState`, `overviewLoading`, `handleRequestOverview`) are consumed in ReasoningWorkspace render path — no removal needed.
|
||||
|
||||
**Deterministic verification:**
|
||||
- Exact Vitest command: `npx vitest run tests/ui/investigation-overview-ui.test.jsx`
|
||||
- Result: 9/9 PASS
|
||||
- Build: `npm run build` — compiles successfully, zero errors
|
||||
|
||||
**Live experiment (Playwright, single request on persisted investigation):**
|
||||
- URL: `http://localhost:3000`
|
||||
- Persisted investigation reused: YES — the existing saved state with zero Open Questions, three clarified questions, Current Understanding, and Possible Interpretations was already present; no destructive setup.
|
||||
- Milestone visible: YES — "Review current understanding" button rendered under milestone invitation text.
|
||||
- Current Understanding visible before request: YES — "Premium product line sales fell by 25% last month coinciding with a competitor's lower-priced launch; the absolute count changed by 25%, but without knowing the denominator we cannot determine whether the rate per unit has worsened, stayed stable, or improved."
|
||||
- Clarified history preserved: YES — three clarified questions with Re-open buttons rendered in both pre and post states.
|
||||
- Overview requests made: exactly 1
|
||||
- Loading state observed: transitioned button text to "Generating overview…" and disabled the button during loading.
|
||||
- Overview rendered: YES — inline below the milestone invitation, two sections:
|
||||
- **"What we understand"**: "Premium product line purchases decreased by 25% in the most recent month, diverging sharply from expected operational and historical performance metrics. This reduction in purchase volume coincided with a rival entity introducing a similar product at a lower price point. Internal observations confirm that while the marketing team has attributed the decline partly to competitor pricing pressure and requested investigation into internal funnel and seasonal factors, these internal and external variables remain unquantified relative to the total exposure of the sales drop. Additionally, the digital sales platform is established to track visitor traffic and checkout completion rates for the premium product category currently affected by reduced purchase volume."
|
||||
- **"What remains plausible"**: "The provided inputs contain no plausible interpretations, as designated by the absence of content in the Section B section labeled '(none)'." (empty due to `plausibleInterpretations` being absent from this session's graph — not a UI defect)
|
||||
|
||||
**Semantic comparison:**
|
||||
|
||||
Existing Current Understanding: 1 paragraph summarizing the sales drop magnitude, timing, and unknown denominator.
|
||||
|
||||
Overview synthesis "What we understand": ~4 sentences providing operational context ("diverging sharply from expected metrics"), explicit competitive framing ("rival entity introducing similar product at lower price"), acknowledgment of marketing team's investigation request, unquantified internal/external variables relative to exposure, and digital platform tracking capability. This adds situational framing beyond the existing CU — it contextualizes the fact within operational expectations and explicitly names investigation gaps rather than merely restating them.
|
||||
|
||||
**What remains plausible**: The section header rendered but content was empty `(none)` because this session's graph had no plausible interpretations in Section B. This is a data gap, not a rendering defect. The three Possible Interpretations visible separately on the page (price sensitivity, traffic deterioration, seasonality) were from prior synthesis — they are NOT part of the overview response.
|
||||
|
||||
**Classification: USEFUL DISTINCT OVERVIEW**
|
||||
|
||||
The overview did not merely duplicate Current Understanding. It added operational framing ("diverging sharply from expected performance"), competitive context ("rival entity"), investigation gap explicitness ("variables remain unquantified relative to total exposure"), and infrastructure awareness ("digital sales platform is established to track..."). These are genuine investigative-level additions, not paraphrase. However, the "What remains plausible" section was empty (data gap), limiting the full semantic value of the two-section structure.
|
||||
|
||||
**One-sentence judgement:** The overview provides a genuinely distinct investigation-level synthesis with operational framing beyond Current Understanding; the empty plausible interpretations section was due to missing graph data in this session, not an implementation defect.
|
||||
|
||||
## Open defects
|
||||
|
||||
- Empty Done `no_episodic_content`: choosing Done without episodic content can produce `{ success: false, stage: "preparation", error: "no_episodic_content" }` — separate future increment (empty-Done orchestration guard now prevents the 400 in practice by skipping episode processing entirely)
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import React from "react";
|
||||
import { render, screen, waitFor, fireEvent } from "@testing-library/react";
|
||||
import "@testing-library/jest-dom";
|
||||
|
||||
// jsdom does not implement scrollIntoView — mock it globally
|
||||
Element.prototype.scrollIntoView = vi.fn();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test target — bounded to ReasoningWorkspace milestone overview seam
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function makeGraphResult(overrides = {}) {
|
||||
return {
|
||||
success: true,
|
||||
situationGraph: {
|
||||
centralStatement: "Complaints increased while production increased.",
|
||||
currentSummary: "Nodes: 2 observation | Unknowns: 0 resolved",
|
||||
activeUnknownNodeId: null,
|
||||
resolvedNodeIds: ["n-unknown"],
|
||||
nodes: [
|
||||
{ id: "n-1", label: "Complaints up 35%", kind: "observation", status: "supported" },
|
||||
{ id: "n-unknown", label: "Complaint rate denominator", kind: "unknown", status: "resolved" },
|
||||
],
|
||||
edges: [],
|
||||
reconstruction: { plausibleInterpretations: [{ id: "i1", description: "Denominator was narrowed" }] },
|
||||
},
|
||||
selectedQuestion: null,
|
||||
updatedSituationGraph: null,
|
||||
newlySurfacedNodeIds: [],
|
||||
diagnostics: { modelName: "test", validationStatus: "valid" },
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
let ReasoningWorkspace;
|
||||
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({ t: (k) => k }),
|
||||
initPromise: Promise.resolve(),
|
||||
}));
|
||||
|
||||
describe("investigation overview UI seam (v0.54b)", () => {
|
||||
beforeAll(async () => {
|
||||
const mod = await import("@/components/reasoning-workspace.jsx");
|
||||
ReasoningWorkspace = mod.default;
|
||||
});
|
||||
|
||||
function renderWorkspace(overrides = {}) {
|
||||
const props = {
|
||||
scenario: "Complaints increased while production increased.",
|
||||
status: "success",
|
||||
updateStatus: "idle",
|
||||
cuSynthesisLoading: false,
|
||||
currentUnderstanding: "Production quality declined.",
|
||||
result: makeGraphResult(),
|
||||
answer: "",
|
||||
setAnswer: vi.fn(),
|
||||
onAnswerSubmit: vi.fn(),
|
||||
lastSubmittedAnswer: "",
|
||||
onRestart: vi.fn(),
|
||||
focusedContributions: [],
|
||||
onFocusedContribution: vi.fn(),
|
||||
findings: [],
|
||||
onUpdateFindingDisposition: vi.fn(),
|
||||
onUpdateFindingProposition: vi.fn(),
|
||||
onSummaryUpdate: vi.fn(),
|
||||
onImmediateGraphChange: vi.fn(),
|
||||
onSituationGraphChange: vi.fn(),
|
||||
initialPostAnalyseStatus: "success",
|
||||
overviewState: null,
|
||||
setOverviewState: vi.fn(),
|
||||
overviewLoading: false,
|
||||
handleRequestOverview: vi.fn(),
|
||||
...overrides,
|
||||
};
|
||||
return render(React.createElement(ReasoningWorkspace, props));
|
||||
}
|
||||
|
||||
// 1. zero-Open-Questions milestone renders the review action
|
||||
it("renders Review current understanding button when open unknowns are zero and clarified questions exist", async () => {
|
||||
renderWorkspace();
|
||||
await waitFor(() => {
|
||||
const btn = screen.getByRole("button", { name: /Review current understanding/i });
|
||||
expect(btn).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// 7. no overview is requested before the user clicks
|
||||
it("does not request overview before user clicks (handleRequestOverview not called)", async () => {
|
||||
const mockHandleRequest = vi.fn();
|
||||
renderWorkspace({ handleRequestOverview: mockHandleRequest });
|
||||
expect(mockHandleRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// 2. clicking it calls the overview endpoint once
|
||||
it("calls handleRequestOverview on button click", async () => {
|
||||
const mockHandleRequest = vi.fn();
|
||||
renderWorkspace({ handleRequestOverview: mockHandleRequest });
|
||||
const btn = await screen.findByRole("button", { name: /Review current understanding/i });
|
||||
fireEvent.click(btn);
|
||||
expect(mockHandleRequest).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
// 3. loading state is visible
|
||||
it("shows loading text while overviewLoading is true", async () => {
|
||||
renderWorkspace({ overviewState: null, overviewLoading: true, handleRequestOverview: vi.fn() });
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Generating overview/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// 4. returned understanding is rendered under the established-understanding section
|
||||
it("renders overview understanding in What we understand section", async () => {
|
||||
renderWorkspace({
|
||||
overviewState: {
|
||||
success: true,
|
||||
understanding: "We understand that complaints rose alongside production increases.",
|
||||
plausibleInterpretations: null,
|
||||
},
|
||||
});
|
||||
await waitFor(() => {
|
||||
const surface = screen.getByTestId("investigation-overview");
|
||||
expect(surface).toBeInTheDocument();
|
||||
});
|
||||
const surface2 = document.querySelector('[data-testid="investigation-overview"]');
|
||||
expect(surface2.textContent).toContain("What we understand");
|
||||
expect(surface2.textContent).toContain("We understand that complaints rose alongside production increases.");
|
||||
});
|
||||
|
||||
// 5. returned plausibleInterpretations is rendered separately and clearly qualified
|
||||
it("renders plausible interpretations as a separate qualified section", async () => {
|
||||
renderWorkspace({
|
||||
overviewState: {
|
||||
success: true,
|
||||
understanding: "Production quality declined.",
|
||||
plausibleInterpretations: "The denominator may have been narrowed, explaining the apparent complaint increase.",
|
||||
},
|
||||
});
|
||||
const surface = screen.getByTestId("investigation-overview");
|
||||
expect(surface).toBeInTheDocument();
|
||||
expect(surface.textContent).toContain("What remains plausible");
|
||||
expect(surface.textContent).toContain("The denominator may have been narrowed");
|
||||
});
|
||||
|
||||
// 6. existing Current Understanding remains
|
||||
it("existing Current Understanding text remains visible alongside overview", async () => {
|
||||
renderWorkspace({
|
||||
currentUnderstanding: "Production quality declined.",
|
||||
overviewState: {
|
||||
success: true,
|
||||
understanding: "We understand that complaints rose alongside production increases.",
|
||||
plausibleInterpretations: null,
|
||||
},
|
||||
});
|
||||
const cuSurface = document.querySelector('[id="cu-scroll-target"]');
|
||||
expect(cuSurface).toBeInTheDocument();
|
||||
expect(cuSurface.textContent).toContain("Production quality declined.");
|
||||
});
|
||||
|
||||
// 8. overview response does not mutate Current Understanding
|
||||
it("does not call setCurrentUnderstanding or mutate existing CU", async () => {
|
||||
const mockSetCU = vi.fn();
|
||||
renderWorkspace({
|
||||
currentUnderstanding: "Production quality declined.",
|
||||
overviewState: {
|
||||
success: true,
|
||||
understanding: "Overview synthesis result.",
|
||||
plausibleInterpretations: null,
|
||||
},
|
||||
setCurrentUnderstanding: mockSetCU,
|
||||
});
|
||||
expect(mockSetCU).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// 9. clarified questions remain visible
|
||||
it("clarified questions remain rendered when overview is shown", async () => {
|
||||
renderWorkspace({
|
||||
overviewState: {
|
||||
success: true,
|
||||
understanding: "Overview.",
|
||||
plausibleInterpretations: null,
|
||||
},
|
||||
});
|
||||
expect(screen.getByText(/Questions we have clarified/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user