fix: show update reasoning progress

This commit is contained in:
2026-08-03 18:00:20 +01:00
parent 7bc1c93486
commit 74fc6d1457
2 changed files with 353 additions and 21 deletions
+70 -13
View File
@@ -5,6 +5,33 @@ import DiagnosticsView from "@/components/diagnostics-view";
import GraphUpdateView from "@/components/graph-update-view";
import SituationGraphView from "@/components/situation-graph-view";
// ── Technical summary detector (main view filters these) ───
const TECHNICAL_PATTERNS = [
/nodes?\s*[:\d]/i,
/edges?\s*[:\d]/i,
/\b(?:unknown|observation|conclusion)\b\s/i,
/\bsorted\b/i,
/by_kind/i,
/\b(?:node|edge|unknown|state)\s+count/i,
];
function isTechnicalSummary(summary) {
if (!summary || typeof summary !== "string") return false;
const trimmed = summary.trim();
if (!trimmed) return false;
for (const p of TECHNICAL_PATTERNS) {
if (p.test(trimmed)) return true;
}
return false;
}
function resolveCurrentSummary(currentSummary) {
if (isTechnicalSummary(currentSummary)) {
return null;
}
return currentSummary || null;
}
// ── Status message pools for loading feedback ────────────────
const INITIAL_MESSAGES = [
{ min: 0, text: "Reading your situation" },
@@ -74,22 +101,20 @@ function SituationCard({ centralStatement }) {
// ── Current understanding card ────────────────────────────────
function CurrentUnderstanding({ currentSummary }) {
if (currentSummary) {
const summary = resolveCurrentSummary(currentSummary);
return (
<div className="investigation-card rounded-lg border border-gray-200 bg-white p-5">
<h2 className="mb-3 text-sm font-semibold uppercase tracking-wide text-gray-500">
What we&#x27;ve established
</h2>
<p className="text-sm leading-relaxed text-gray-700">{currentSummary}</p>
</div>
);
}
return (
<div className="investigation-card rounded-lg border border-gray-200 bg-white p-5">
{summary ? (
<p className="text-sm leading-relaxed text-gray-700">{summary}</p>
) : (
<p className="text-sm leading-relaxed text-gray-600">
We have started to separate what is known from what still needs checking.
We have separated what is known from what still needs checking.
</p>
)}
</div>
);
}
@@ -111,6 +136,22 @@ function CurrentInvestigationCard({ selectedQuestion }) {
);
}
// ── Outcome helpers ───────────────────────────────────────────
function hasGenuineCompletion(graph) {
if (!graph || !graph.nodes?.length) return false;
const resolvedIds = new Set(graph.resolvedNodeIds || []);
const unresolvedCount = graph.nodes.filter(
(n) => n.kind === "unknown" && n.status !== "resolved" && !resolvedIds.has(n.id),
).length;
if (unresolvedCount > 0) return false;
if (graph.activeUnknownNodeId) {
const active = graph.nodes.find((n) => n.id === graph.activeUnknownNodeId);
if (active && active.status !== "resolved" && !resolvedIds.has(active.id)) return false;
}
return true;
}
// ── Investigation progress card ──────────────────────────────
function InvestigationProgress({ graph, noQuestionReason: rwNoQuestionReason }) {
if (!graph?.nodes?.length) return null;
@@ -124,9 +165,11 @@ function InvestigationProgress({ graph, noQuestionReason: rwNoQuestionReason })
? graph.nodes.find((n) => n.id === graph.activeUnknownNodeId)
: null;
const isComplete = hasGenuineCompletion(graph);
return (
<div className="investigation-card rounded-lg border border-gray-200 bg-white px-5 py-4">
{remainingCount > 0 && !rwNoQuestionReason ? (
{remainingCount > 0 && !isComplete ? (
<p className="text-sm leading-relaxed text-gray-700">
We are still building confidence about your situation.{" "}
{remainingCount === 1
@@ -262,6 +305,13 @@ export default function ReasoningWorkspace({
const newlySurfacedNodeIds = result?.newlySurfacedNodeIds || [];
const noQuestionReason = diagnostics?.noQuestionReason ?? null;
const remainingUnknowns = graph?.nodes?.filter(
(n) => n.kind === "unknown" && n.status !== "resolved" && !(graph.resolvedNodeIds || []).includes(n.id),
);
const genuineCompletion = hasGenuineCompletion(graph);
const unresolvedRemaining = !genuineCompletion && remainingUnknowns ? remainingUnknowns.length > 0 : false;
return (
<div className="space-y-5">
{/* ── Loading overlays ─────────────────────────────── */}
@@ -290,10 +340,17 @@ export default function ReasoningWorkspace({
</div>
) : (
<>
{/* When investigation has nothing further to ask */}
{status === "success" && !canAnswer && graph && (
{/* Completion or holding state (only when there is no next question) */}
{status === "success" && !canAnswer && graph && genuineCompletion && (
<InvestigationCompleteMessage noQuestionReason={noQuestionReason} />
)}
{status === "success" && !canAnswer && graph && unresolvedRemaining && (
<div className="rounded-lg border border-gray-200 bg-gray-50 px-5 py-4 text-center">
<p className="text-sm text-gray-600">There is no further question the engine can justify at the moment.</p>
<p className="mt-1 text-xs text-gray-500">More evidence may be needed before a next step is clear.</p>
</div>
)}
{graph && <SituationCard centralStatement={graph.centralStatement} />}
{graph && <CurrentUnderstanding currentSummary={graph.currentSummary} />}
{canAnswer && <CurrentInvestigationCard selectedQuestion={selectedQ} />}
@@ -363,4 +420,4 @@ export default function ReasoningWorkspace({
);
}
export { useLoadingStatus, INITIAL_MESSAGES, UPDATE_MESSAGES, LoadingOverlay };
export { useLoadingStatus, INITIAL_MESSAGES, UPDATE_MESSAGES, LoadingOverlay, resolveCurrentSummary, isTechnicalSummary };
+279 -4
View File
@@ -9,6 +9,8 @@ import ReasoningWorkspace, {
INITIAL_MESSAGES,
UPDATE_MESSAGES,
LoadingOverlay,
resolveCurrentSummary,
isTechnicalSummary,
} from "@/components/reasoning-workspace.jsx";
import {
ScenarioResultPanels,
@@ -885,14 +887,14 @@ describe("ReasoningWorkspace UI", () => {
expect(html).toContain("Complaints increased while production increased.");
});
it("shows what we've established section", () => {
it("shows what we've established section with plain-language summary", () => {
const html = renderToStaticMarkup(
<ReasoningWorkspace
{...makeWorkspaceProps({
result: makeWorkspaceResult({
situationGraph: {
...makeWorkspaceResult().situationGraph,
currentSummary: "Nodes: 2 observation, 1 unknown",
currentSummary: "We have identified a key question to investigate further.",
},
}),
})}
@@ -900,7 +902,27 @@ describe("ReasoningWorkspace UI", () => {
);
expect(html).toContain("What we&#x27;ve established");
expect(html).toContain("Nodes: 2 observation, 1 unknown");
expect(html).toContain("We have identified a key question to investigate further.");
});
it("filters technical summaries from the main view (not developer details)", () => {
const html = renderToStaticMarkup(
<ReasoningWorkspace {...makeWorkspaceProps()} />,
);
// Fallback text shown in main view (currentSummary filtered)
expect(html).toContain("We have separated what is known from what still needs checking.");
// Developer details preserves the full technical summary
expect(html).toContain("Developer details");
});
it("keeps technical summary available in Developer details", () => {
const html = renderToStaticMarkup(
<ReasoningWorkspace {...makeWorkspaceProps()} />,
);
// Developer details is the mechanism for accessing full technical data
expect(html).toContain("Developer details");
});
it("prominently displays the current investigation", () => {
@@ -1115,6 +1137,10 @@ describe("ReasoningWorkspace UI", () => {
// After 16s elapsed, the second message should be active
// (useEffect fires in real React; here we verify via hook export)
expect(INITIAL_MESSAGES[1].min).toBe(10);
expect(UPDATE_MESSAGES[0].text).toBe("Considering your answer");
expect(UPDATE_MESSAGES[1].text).toBe("Updating the situation");
expect(UPDATE_MESSAGES[2].text).toBe("Checking what changed");
expect(UPDATE_MESSAGES[3].text).toBe("Choosing the next question");
vi.useRealTimers();
});
@@ -1141,6 +1167,14 @@ describe("ReasoningWorkspace UI", () => {
updateStatus="idle"
result={makeWorkspaceResult({
selectedQuestion: null,
situationGraph: {
...makeWorkspaceResult().situationGraph,
activeUnknownNodeId: null,
nodes: makeWorkspaceResult().situationGraph.nodes.map((n) =>
n.id === "n-unknown" ? { ...n, status: "resolved", value: "the denominator is total units sold" } : n,
),
resolvedNodeIds: ["n-unknown"],
},
diagnostics: { ...makeWorkspaceResult().diagnostics, noQuestionReason: "All unknowns resolved" },
})}
answer=""
@@ -1241,6 +1275,39 @@ describe("ReasoningWorkspace UI", () => {
expect(html).not.toContain("Update situation");
});
// ── Technical summary detection ───────────────────────
it("isTechnicalSummary detects node counts", () => {
expect(isTechnicalSummary("Nodes: 2 observation, 1 unknown")).toBe(true);
expect(isTechnicalSummary("Nodes: 3 observations total")).toBe(true);
});
it("isTechnicalSummary detects edge counts", () => {
expect(isTechnicalSummary("Edges: 5 total")).toBe(true);
expect(isTechnicalSummary("Edges: 2")).toBe(true);
});
it("isTechnicalSummary detects graph enum language", () => {
expect(isTechnicalSummary("1 unknown remaining")).toBe(true);
expect(isTechnicalSummary("Observation nodes identified")).toBe(true);
expect(isTechnicalSummary("Conclusion verified")).toBe(true);
});
it("isTechnicalSummary allows plain-language summaries", () => {
expect(isTechnicalSummary("We have separated what is known from what still needs checking.")).toBe(false);
expect(isTechnicalSummary("Updated summary")).toBe(false);
expect(isTechnicalSummary("")).toBe(false);
expect(isTechnicalSummary(null)).toBe(false);
expect(isTechnicalSummary(undefined)).toBe(false);
});
it("resolveCurrentSummary filters technical and passes plain", () => {
expect(resolveCurrentSummary("Nodes: 1 state, 3 observations")).toBe(null);
expect(resolveCurrentSummary("Updated summary")).toBe("Updated summary");
expect(resolveCurrentSummary(null)).toBe(null);
expect(resolveCurrentSummary(undefined)).toBe(null);
});
it("update result merges into workspace correctly", () => {
const html = renderToStaticMarkup(
<ReasoningWorkspace
@@ -1524,6 +1591,14 @@ describe("ReasoningWorkspace UI", () => {
updateStatus="idle"
result={makeWorkspaceResult({
selectedQuestion: null,
situationGraph: {
...makeWorkspaceResult().situationGraph,
activeUnknownNodeId: null,
nodes: makeWorkspaceResult().situationGraph.nodes.map((n) =>
n.id === "n-unknown" ? { ...n, status: "resolved", value: "1.9 complaints per 100 units" } : n,
),
resolvedNodeIds: ["n-unknown"],
},
diagnostics: { ...makeWorkspaceResult().diagnostics, noQuestionReason: "All unknowns satisfied" },
})}
answer=""
@@ -1536,13 +1611,21 @@ describe("ReasoningWorkspace UI", () => {
expect(html).not.toContain("There is no next question at the moment");
});
it("investigation complete shows confidence-building fallback", () => {
it("no-question state with unresolved areas shows holding, not completion", () => {
const html = renderToStaticMarkup(
<ReasoningWorkspace
status="success"
updateStatus="idle"
result={makeWorkspaceResult({
selectedQuestion: null,
situationGraph: {
...makeWorkspaceResult().situationGraph,
activeUnknownNodeId: "n-unknown",
nodes: makeWorkspaceResult().situationGraph.nodes.map((n) =>
n.id === "n-unknown" ? { ...n, status: "unknown", value: null } : n,
),
resolvedNodeIds: [],
},
diagnostics: { ...makeWorkspaceResult().diagnostics, noQuestionReason: null },
})}
answer=""
@@ -1551,6 +1634,198 @@ describe("ReasoningWorkspace UI", () => {
/>,
);
expect(html).toContain("There is no further question the engine can justify at the moment");
expect(html).not.toContain("We have established enough for now");
expect(html).not.toContain("All areas under investigation are now complete");
});
// ── Update loading tests ───────────────────────────────
it("update loading card appears immediately after answer submit", () => {
const html = renderToStaticMarkup(
<ReasoningWorkspace
status="success"
updateStatus="loading"
result={makeWorkspaceResult()}
answer="some answer"
setAnswer={vi.fn()}
onAnswerSubmit={vi.fn()}
/>,
);
expect(html).toContain("Working through your situation");
expect(html).toContain("Considering your answer");
});
it("normal update button is hidden while loading", () => {
const html = renderToStaticMarkup(
<ReasoningWorkspace
status="success"
updateStatus="loading"
result={makeWorkspaceResult()}
answer="some answer"
setAnswer={vi.fn()}
onAnswerSubmit={vi.fn()}
/>,
);
expect(html).not.toContain("Update situation");
expect(html).not.toContain("Your response");
});
it("update status messages change with mocked timers", () => {
vi.useFakeTimers();
const baseProps = {
status: "success",
updateStatus: "loading",
result: makeWorkspaceResult(),
answer: "test",
setAnswer: vi.fn(),
onAnswerSubmit: vi.fn(),
};
let html = renderToStaticMarkup(<ReasoningWorkspace {...baseProps} />);
expect(html).toContain("Considering your answer");
vi.advanceTimersByTime(10000);
vi.useRealTimers();
// We rely on the exported UPDATE_MESSAGES to verify message pool correctness
expect(UPDATE_MESSAGES[0].text).toBe("Considering your answer");
expect(UPDATE_MESSAGES[1].text).toBe("Updating the situation");
expect(UPDATE_MESSAGES[2].text).toBe("Checking what changed");
expect(UPDATE_MESSAGES[3].text).toBe("Choosing the next question");
vi.useRealTimers();
});
it("update prevents duplicate submission by removing controls while loading", () => {
const html = renderToStaticMarkup(
<ReasoningWorkspace
status="success"
updateStatus="loading"
result={makeWorkspaceResult()}
answer="some answer"
setAnswer={vi.fn()}
onAnswerSubmit={vi.fn()}
/>,
);
expect(html).not.toContain("Update situation");
expect(html).toContain("Working through your situation");
});
// ── Outcome state tests ────────────────────────────────
it("next-question state does not show completion wording", () => {
const html = renderToStaticMarkup(
<ReasoningWorkspace
{...makeWorkspaceProps({
result: makeWorkspaceResult({
selectedQuestion: { question: "What denominator is being used?" },
}),
})}
/>,
);
expect(html).toContain("Current investigation");
expect(html).not.toContain("We have established enough for now");
expect(html).not.toContain("All areas under investigation are now complete");
});
it("unresolved-no-question state does not imply completion", () => {
const html = renderToStaticMarkup(
<ReasoningWorkspace
status="success"
updateStatus="idle"
result={makeWorkspaceResult({
selectedQuestion: null,
situationGraph: {
...makeWorkspaceResult().situationGraph,
activeUnknownNodeId: "n-unknown",
nodes: [
...makeWorkspaceResult().situationGraph.nodes.filter(
(n) => n.id !== "n-unknown" || n.kind === "observation",
),
{
id: "n-unknown",
label: "Complaint rate denominator",
description: "Need the denominator for complaint rate",
kind: "unknown",
status: "unknown",
confidence: "medium",
},
],
resolvedNodeIds: [],
},
})}
answer=""
setAnswer={vi.fn()}
onAnswerSubmit={vi.fn()}
/>,
);
expect(html).not.toContain("All areas under investigation are now complete");
expect(html).not.toContain("We have established enough for now");
expect(html).toContain("There is no further question the engine can justify at the moment");
});
it("genuine completion hides active investigation and remaining-area wording", () => {
const html = renderToStaticMarkup(
<ReasoningWorkspace
status="success"
updateStatus="idle"
result={makeWorkspaceResult({
selectedQuestion: null,
situationGraph: {
...makeWorkspaceResult().situationGraph,
activeUnknownNodeId: null,
nodes: makeWorkspaceResult().situationGraph.nodes.map((n) =>
n.id === "n-unknown" ? { ...n, status: "resolved", value: "total units" } : n,
),
resolvedNodeIds: ["n-unknown"],
},
})}
answer=""
setAnswer={vi.fn()}
onAnswerSubmit={vi.fn()}
/>,
);
expect(html).not.toContain("Current investigation");
expect(html).not.toContain("areas remain");
expect(html).toContain("We have established enough for now");
});
it("no contradictory completion and unresolved focus appear together", () => {
const html = renderToStaticMarkup(
<ReasoningWorkspace
status="success"
updateStatus="idle"
result={makeWorkspaceResult({
selectedQuestion: null,
situationGraph: {
...makeWorkspaceResult().situationGraph,
activeUnknownNodeId: "n-unknown",
resolvedNodeIds: [],
},
})}
answer=""
setAnswer={vi.fn()}
onAnswerSubmit={vi.fn()}
/>,
);
const hasCompletion = html.includes("All areas under investigation are now complete") || html.includes("We have established enough for now");
expect(hasCompletion).toBe(false);
});
it("Developer details remains collapsed and unchanged", () => {
const html = renderToStaticMarkup(
<ReasoningWorkspace {...makeWorkspaceProps()} />,
);
expect(html).toContain("Developer details");
});
});