feat: surface new unknowns after graph updates

This commit is contained in:
2026-08-02 10:30:59 +01:00
parent 904aec7616
commit 72ef175971
16 changed files with 910 additions and 41 deletions
+18 -3
View File
@@ -23,12 +23,17 @@ export default function GraphUpdateView({ updateResult }) {
affectedNodeIds,
previousActiveUnknownNodeId,
newActiveUnknownNodeId,
selectedQuestion,
changesApplied,
proposal,
previousSituationGraph,
updatedSituationGraph,
} = updateResult;
const newlySurfacedUnknownNodeIds = (proposal.addedNodes || [])
.filter((node) => node.kind === "unknown")
.map((node) => node.id);
const previousNodesById = new Map(
(previousSituationGraph?.nodes || []).map((node) => [node.id, node]),
);
@@ -123,10 +128,15 @@ export default function GraphUpdateView({ updateResult }) {
{resolveActiveUnknown(newActiveUnknownNodeId)}
</div>
)}
{!newActiveUnknownNodeId && previousActiveUnknownNodeId && (
{selectedQuestion?.question && (
<div>
<span className="font-medium">Next question status:</span> No next
question selected yet.
<span className="font-medium">Next question:</span>{" "}
{selectedQuestion.question}
</div>
)}
{!selectedQuestion?.question && !newActiveUnknownNodeId && previousActiveUnknownNodeId && (
<div>
<span className="font-medium">Next question status:</span> No next question selected yet.
</div>
)}
</div>
@@ -137,6 +147,11 @@ export default function GraphUpdateView({ updateResult }) {
items={resolvedUnknownNodeIds}
renderItem={resolveNodePresentation}
/>
<ListSection
title="Newly surfaced unknowns"
items={newlySurfacedUnknownNodeIds}
renderItem={resolveNodePresentation}
/>
<ListSection
title="Affected nodes"
items={affectedNodeIds}
+56 -2
View File
@@ -52,9 +52,16 @@ function normaliseStartResult(data) {
typeof data?.selectedQuestion === "string"
? data.selectedQuestion
: data?.selectedQuestion?.question ?? null,
newlySurfacedNodeIds: data?.newlySurfacedNodeIds ?? [],
};
}
function normaliseUpdateSelectedQuestion(selectedQuestion) {
if (!selectedQuestion) return null;
if (typeof selectedQuestion === "string") return selectedQuestion;
return selectedQuestion.question ?? null;
}
export function ScenarioResultPanels({ status, result }) {
if (!result) return null;
@@ -83,6 +90,7 @@ export function ScenarioResultPanels({ status, result }) {
<SituationGraphView
situationGraph={result.situationGraph}
selectedQuestion={result.selectedQuestion}
newlySurfacedNodeIds={result.newlySurfacedNodeIds}
/>
)}
@@ -192,7 +200,12 @@ export default function ScenarioForm() {
setResult((current) => ({
...current,
situationGraph: outcome.updatedSituationGraph,
selectedQuestion: null,
selectedQuestion: normaliseUpdateSelectedQuestion(
outcome.selectedQuestion,
),
newlySurfacedNodeIds: (outcome.proposal?.addedNodes || [])
.filter((node) => node.kind === "unknown")
.map((node) => node.id),
diagnostics: outcome.diagnostics,
}));
setAnswer("");
@@ -208,9 +221,14 @@ export default function ScenarioForm() {
const canRenderAnswerForm =
status === "success" &&
updateStatus === "idle" &&
Boolean(result?.situationGraph) &&
Boolean(result?.selectedQuestion);
const canRenderDisabledFollowUpForm =
updateStatus === "success" &&
Boolean(updateResult?.selectedQuestion?.question || result?.selectedQuestion);
return (
<div className="space-y-6">
<form onSubmit={handleSubmit} className="space-y-4">
@@ -277,8 +295,44 @@ export default function ScenarioForm() {
{updateStatus === "success" && updateResult && (
<>
<div className="rounded-lg border border-yellow-300 bg-yellow-50 px-4 py-3 text-sm text-yellow-800">
No next question selected yet.
{updateResult.selectedQuestion?.question
? updateResult.selectedQuestion.question
: "No next question selected yet."}
</div>
{canRenderDisabledFollowUpForm && (
<form className="space-y-4 rounded-lg border border-gray-200 bg-white p-4 opacity-70">
<div>
<h2 className="text-base font-semibold text-gray-900">Selected Question</h2>
<p className="mt-1 text-sm text-gray-700">
{updateResult.selectedQuestion?.question || result?.selectedQuestion}
</p>
</div>
<div>
<label htmlFor="follow-up-disabled-textarea" className="mb-2 block text-sm font-medium text-gray-700">
Your answer
</label>
<textarea
id="follow-up-disabled-textarea"
rows={4}
disabled
className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm opacity-70"
placeholder="Additional submission is disabled in this one-update prototype."
/>
</div>
<div className="flex items-center justify-between gap-4">
<p className="text-xs text-gray-500">
Additional submission is disabled in this one-update prototype.
</p>
<button
type="button"
disabled
className="rounded-lg bg-blue-700 px-4 py-2 text-sm font-medium text-white disabled:cursor-not-allowed disabled:opacity-40"
>
Update situation
</button>
</div>
</form>
)}
<GraphUpdateView updateResult={updateResult} />
</>
)}
+25 -1
View File
@@ -8,6 +8,8 @@ function NodeBadge({ children, tone = "gray" }) {
blue: "border-blue-200 bg-blue-50 text-blue-700",
green: "border-green-200 bg-green-50 text-green-700",
yellow: "border-yellow-200 bg-yellow-50 text-yellow-700",
red: "border-red-200 bg-red-50 text-red-700",
purple: "border-purple-200 bg-purple-50 text-purple-700",
};
return (
@@ -17,7 +19,13 @@ function NodeBadge({ children, tone = "gray" }) {
);
}
function NodeGroup({ title, nodes }) {
function NodeGroup({
title,
nodes,
resolvedNodeIds = new Set(),
newlySurfacedNodeIds = new Set(),
activeUnknownNodeId = null,
}) {
if (!nodes?.length) return null;
return (
@@ -32,6 +40,15 @@ function NodeGroup({ title, nodes }) {
<span className="font-medium text-gray-900">{node.label}</span>
<NodeBadge tone="blue">{node.status}</NodeBadge>
<NodeBadge tone="green">{node.confidence}</NodeBadge>
{resolvedNodeIds.has(node.id) && (
<NodeBadge tone="red">resolved unknown</NodeBadge>
)}
{newlySurfacedNodeIds.has(node.id) && (
<NodeBadge tone="purple">newly surfaced unknown</NodeBadge>
)}
{activeUnknownNodeId === node.id && (
<NodeBadge tone="yellow">active unknown</NodeBadge>
)}
{node.value != null && (
<NodeBadge tone="yellow">
{node.value}
@@ -52,6 +69,7 @@ function NodeGroup({ title, nodes }) {
export default function SituationGraphView({
situationGraph,
selectedQuestion,
newlySurfacedNodeIds = [],
}) {
if (!situationGraph) return null;
@@ -70,6 +88,9 @@ export default function SituationGraphView({
return acc;
}, {});
const resolvedNodeIdSet = new Set(situationGraph.resolvedNodeIds || []);
const newlySurfacedNodeIdSet = new Set(newlySurfacedNodeIds || []);
return (
<div className="space-y-4">
{selectedQuestionText && (
@@ -110,6 +131,9 @@ export default function SituationGraphView({
key={kind}
title={kind.replace(/_/g, " ")}
nodes={nodes}
resolvedNodeIds={resolvedNodeIdSet}
newlySurfacedNodeIds={newlySurfacedNodeIdSet}
activeUnknownNodeId={situationGraph.activeUnknownNodeId}
/>
))}