experiment: facilitator progress panel (Version B)

This commit is contained in:
2026-08-05 14:37:43 +01:00
parent 54acf0d565
commit ebea15c970
4 changed files with 361 additions and 3 deletions
+12
View File
@@ -422,3 +422,15 @@ The active conversation has a stable spatial home. Question, Response, and Histo
Supporting artefacts should remain spatially stable while the conversation grows. Desktop width should be used to preserve context, not merely enlarge cards. Text should not be truncated when sufficient readable space exists.
Mobile remains a natural stacked flow with no horizontal split.
## Facilitator Translation Layer (Experiment 11 — Emerging)
The reasoning engine produces a rich graph with structured concepts (observations, unknowns, assumptions, relationships, metrics, states). The UI should increasingly become a translation layer over this graph rather than maintaining separate duplicated summaries.
For end users, present the same data as:
- **Known** — resolved nodes and established observations
- **Still investigating** — unresolved unknowns and assumptions to validate
- **Quiet reasoning summary** — raw counts (nodes, edges, etc.) visually secondary
Internal graph concepts should remain available for developers (Developer Details) but should not dominate the primary view. The panel should feel like a facilitator's notebook: someone looking at it should immediately understand where the investigation stands, what has been learned, and what remains uncertain — without needing to understand graph theory.
@@ -0,0 +1,257 @@
/**
* InvestigationSummaryPanelV2 — Phase 4, Experiment 11
* A facilitator-style progress panel that translates the reasoning graph
* into a human-friendly "what is known / what remains" view.
*
* Design principle:
* The UI should progressively become a translation layer over the
* reasoning graph rather than maintaining separate duplicated summaries.
* Internal graph concepts remain available for developers, while end
* users see a facilitator-style explanation of what is currently understood
* and what remains uncertain.
*
* This component uses exactly the same graph data as InvestigationSummaryPanel
* (Version A). No new backend fields or API contracts are required.
*/
/* ── Helpers ──────────────────────────────────────────────── */
function formatTimestamp(iso) {
if (!iso) return "—";
try {
const d = new Date(iso);
if (isNaN(d)) return iso;
const pad = (n) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
} catch {
return iso;
}
}
function humaniseDuration(seconds) {
if (!seconds || seconds < 0) return "—";
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
if (mins === 0) return `${secs}s`;
return `${mins}m ${secs}s`;
}
/* ── Data extraction helpers ─────────────────────────────── */
/**
* Classify nodes into "known" (resolved / observations with values) and
* "still investigating" (unresolved unknowns and assumptions needing validation).
*/
function classifyNodes(graph, resolvedIds) {
if (!graph?.nodes) return { known: [], stillInvestigating: [] };
const resolved = new Set(resolvedIds || []);
const known = [];
const stillInvestigating = [];
for (const node of graph.nodes) {
const isResolved = resolved.has(node.id) || node.status === "resolved";
// Resolved nodes become known facts
if (isResolved) {
known.push({
label: node.label,
description: node.description,
kind: node.kind,
confidence: node.confidence,
});
} else {
// Unresolved unknowns and assumptions go into "still investigating"
stillInvestigating.push({
label: node.label,
description: node.description,
kind: node.kind,
confidence: node.confidence,
});
}
}
return { known, stillInvestigating };
}
/**
* Map graph node kinds to end-user-friendly group labels.
*/
function groupLabelForKind(kind) {
const map = {
unknown: "Still investigating",
assumption: "Assumptions to validate",
observation: "Observations",
state: "Current states",
metric: "Metrics",
conclusion: "Conclusions",
};
return map[kind] || kind.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
}
/* ── Rendering helpers ───────────────────────────────────── */
/**
* Render a single item from the known or still-investigating lists.
* Show only meaningful content — hide labels that duplicate description.
*/
function renderListItem(item) {
// Prefer description if it adds something beyond the label
const text = (item.description && item.description !== item.label)
? item.description
: item.label;
return text;
}
/* ── Component ────────────────────────────────────────────── */
function InvestigationSummaryPanelV2({ graph, selectedQuestion, result, updateStatus }) {
// ── Status (same derivation logic as Version A) ──────────
const isInvestigating = Boolean(selectedQuestion);
const hasGraph = Boolean(graph);
let currentStatus;
if (updateStatus === "loading") {
currentStatus = { label: "Reasoning", level: "investigating" };
} else if (!hasGraph) {
currentStatus = { label: "Not started", level: "idle" };
} else if (isInvestigating) {
currentStatus = { label: "Investigation in progress", level: "investigating" };
} else if (graph.resolvedNodeIds?.length > 0 && graph.nodes) {
const unresolvedUnknowns = graph.nodes.filter(
(n) => n.kind === "unknown" && !graph.resolvedNodeIds.includes(n.id)
);
if (unresolvedUnknowns.length === 0) {
currentStatus = { label: "Investigation complete", level: "complete" };
} else {
currentStatus = { label: "Current evidence limit reached", level: "limit" };
}
} else {
currentStatus = { label: "Analysis complete", level: "complete" };
}
const statusColors = {
idle: { border: "border-gray-200/60", bg: "bg-gray-50/40", text: "text-gray-400" },
investigating: { border: "border-blue-200/60", bg: "bg-blue-50/30", text: "text-blue-600" },
complete: { border: "border-green-200/60", bg: "bg-green-50/30", text: "text-green-600" },
limit: { border: "border-gray-200", bg: "bg-gray-50/40", text: "text-gray-400" },
};
const colors = statusColors[currentStatus.level] || statusColors.idle;
// ── Current understanding (same source as Version A) ────
const currentUnderstanding =
result?.summary ||
result?.updatedSituationGraph?.currentSummary ||
graph?.currentSummary ||
null;
// ── Classify graph data ─────────────────────────────────
const resolvedIds = new Set(graph?.resolvedNodeIds || []);
const { known, stillInvestigating } = classifyNodes(graph, resolvedIds);
// Group still-investigating items by kind for a cleaner view
const investigatingByGroup = {};
for (const item of stillInvestigating) {
const key = groupLabelForKind(item.kind);
if (!investigatingByGroup[key]) investigatingByGroup[key] = [];
investigatingByGroup[key].push(item);
}
// ── Reasoning summary counts (quiet, at bottom) ─────────
const reasonCounts = {
observations: graph?.nodes?.filter((n) => n.kind === "observation").length || 0,
unknowns: stillInvestigating.filter((n) => n.kind === "unknown").length || 0,
assumptions: graph?.nodes?.filter((n) => n.kind === "assumption" && !resolvedIds.has(n.id)).length || 0,
relationships: graph?.edges?.length || 0,
metrics: graph?.nodes?.filter((n) => n.kind === "metric").length || 0,
states: graph?.nodes?.filter((n) => n.kind === "state").length || 0,
conclusions: graph?.nodes?.filter((n) => n.kind === "conclusion").length || 0,
};
// Only show non-zero counts in the reasoning summary
const reasonEntries = Object.entries(reasonCounts).filter(([_, v]) => v > 0);
return (
<div className={`rounded-lg border ${colors.border} ${colors.bg} p-5 space-y-4`}>
{/* Status — minimal indicator */}
<div className="flex items-center gap-2">
<span className={`inline-block h-2.5 w-2.5 rounded-full bg-current ${colors.text}`} />
<span className={`text-sm font-medium ${colors.text}`}>{currentStatus.label}</span>
</div>
{/* ── Current understanding (if any) ──────────────── */}
{currentUnderstanding && (
<div>
<p className="text-sm leading-relaxed text-gray-600">{currentUnderstanding}</p>
</div>
)}
{/* ── Still investigating — primary focus ─────────── */}
{(stillInvestigating.length > 0 || known.length === 0) && (
<div>
{stillInvestigating.length > 1 ? (
<>
<h3 className="mb-2 text-xs font-medium text-gray-400">Still investigating</h3>
<ul className="space-y-1.5">
{Object.entries(investigatingByGroup).map(([group, items]) => (
<li key={group}>
<span className="text-xs font-medium text-gray-500">{group}</span>
<ul className="mt-1 space-y-1">
{items.map((item, i) => (
<li key={i} className="flex items-start gap-2">
<span className="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-blue-400/60" />
<span className="text-sm text-gray-700">
{renderListItem(item)}
</span>
</li>
))}
</ul>
</li>
))}
</ul>
</>
) : stillInvestigating.length === 1 ? (
<div className="flex items-start gap-2">
<span className="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-blue-400/60" />
<p className="text-sm text-gray-700">{renderListItem(stillInvestigating[0])}</p>
</div>
) : null}
</div>
)}
{/* ── What we have learned ────────────────────────── */}
{known.length > 0 && (
<div>
<h3 className="mb-2 text-xs font-medium text-gray-400">What we know</h3>
<ul className="space-y-1.5">
{known.map((item, i) => (
<li key={i} className="flex items-start gap-2">
<span className="mt-1 h-4 w-4 shrink-0 rounded-full bg-green-400/30" style={{ fontSize: "8px", lineHeight: "1" }}></span>
<span className="text-sm text-gray-700">
{renderListItem(item)}
</span>
</li>
))}
</ul>
</div>
)}
{/* ── Quiet reasoning summary — secondary ─────────── */}
<div className="pt-2 border-t border-gray-200/40">
<p className="text-[10px] font-medium tracking-widest uppercase text-gray-300 mb-1.5">Reasoning</p>
<div className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-gray-400">
{reasonEntries.map(([label, count]) => (
<span key={label}>
{count} {label}
</span>
))}
</div>
</div>
</div>
);
}
export default InvestigationSummaryPanelV2;
+29 -3
View File
@@ -5,6 +5,7 @@ import DiagnosticsView from "@/components/diagnostics-view";
import GraphUpdateView from "@/components/graph-update-view";
import SituationGraphView from "@/components/situation-graph-view";
import InvestigationSummaryPanel from "@/components/investigation-summary-panel";
import InvestigationSummaryPanelV2 from "@/components/investigation-summary-panel-v2";
import InvestigationMap from "@/components/investigation-map";
// ── Technical summary detector (main view filters these) ───
@@ -523,6 +524,8 @@ export default function ReasoningWorkspace({
const [investigationHistory, setInvestigationHistory] = useState([]);
const turnCounter = useRef(0);
const pendingTurnRef = useRef(null);
// ── Experiment 11: toggle between progress panel versions ──
const [showVersionB, setShowVersionB] = useState(false);
const { saveSession, loadSession } = useSessionPersistence();
// Persist workspace state on every successful update (Phase 3)
@@ -714,9 +717,32 @@ export default function ReasoningWorkspace({
{hasCurrentSummaryCondition && (
<>
<CurrentUnderstandingCard currentSummary={graph?.currentSummary || result?.updatedSituationGraph?.currentSummary} plainLanguage={propUnderstanding || null} />
<div className="opacity-75">
<InvestigationSummaryPanel graph={graph} selectedQuestion={selectedQ} result={result} updateStatus={updateStatus} />
</div>
{/* ── Experiment 11: progress panel A / B toggle ── */}
{hasGraph && (
<div className="space-y-2">
<div className="flex items-center gap-2">
<button
onClick={() => setShowVersionB(false)}
className={`text-xs transition ${!showVersionB ? "font-medium text-gray-700 underline" : "text-gray-400 hover:text-gray-500"}`}
>
Panel A
</button>
<span className="text-gray-300">/</span>
<button
onClick={() => setShowVersionB(true)}
className={`text-xs transition ${showVersionB ? "font-medium text-gray-700 underline" : "text-gray-400 hover:text-gray-500"}`}
>
Panel B
</button>
</div>
<div className="opacity-75">
{showVersionB
? <InvestigationSummaryPanelV2 graph={graph} selectedQuestion={selectedQ} result={result} updateStatus={updateStatus} />
: <InvestigationSummaryPanel graph={graph} selectedQuestion={selectedQ} result={result} updateStatus={updateStatus} />
}
</div>
</div>
)}
</>
)}
</div>
+63
View File
@@ -470,6 +470,69 @@ A persistent two-thirds conversation column beside a one-third supporting column
#### Evaluation
Visual review completed.
#### Status
Closed.
## Result
Partially confirmed.
## What did we learn?
- The investigation workspace is beginning to feel like a genuine facilitated investigation rather than a document.
- The two-column workspace (conversation on the left, reference material on the right) is proving to be a stronger mental model than previous layouts.
- Keeping Situation and Investigation Map fixed while History grows vertically feels more natural.
- The investigation question, response and history now read as one continuous conversation.
- Developer Details have become extremely valuable.
- The graph produced by the reasoning engine is far richer than previously realised. The graph now contains structured concepts including:
- observations
- unknowns
- assumptions
- relationships
- metrics
- state
This suggests the UI should increasingly become a human-friendly projection of the graph rather than inventing separate state.
The current "Investigation in progress" panel exposes developer-oriented statistics (nodes, edges, unknowns etc.) which are useful during development but are not the most helpful representation for an end user.
---
## Emerging Direction — Facilitator Translation Layer
> The UI should progressively become a translation layer over the reasoning graph rather than maintaining separate duplicated summaries. Internal graph concepts should remain available for developers, while end users see a facilitator-style explanation of what is currently understood and what remains uncertain.
The current technical progress panel (nodes, edges, unknowns, assumptions) exposes developer-oriented statistics. These are valuable during development but not the most helpful representation for an end user.
The next direction is to explore presenting the same underlying graph data as a facilitator's notebook — what is known, what remains uncertain, and a quiet summary of the reasoning state underneath.
---
### Experiment 11 — Facilitator Progress Panel (Version B)
#### Hypothesis
The same underlying reasoning graph can be presented in a much more human-friendly way without changing the reasoning engine, API contracts, or graph generation.
A facilitator-style panel should communicate:
- what is known (resolved nodes and observations)
- what remains uncertain (unresolved unknowns and assumptions)
- a quiet summary of the reasoning state underneath
#### Questions
- Can the same graph data be translated into a facilitator-style view that end users understand more naturally?
- Does separating "known" from "still investigating" reduce cognitive load compared to node/edge counts?
- Is a quiet reasoning summary sufficient, or does it need more context?
- Does the translation-layer principle hold — presenting the graph as a notebook rather than raw data?
#### Evaluation
Pending visual review.
#### Status