test(ui): make inferred questions originate branches

This commit is contained in:
2026-08-20 14:29:42 +01:00
parent 65ced2e406
commit 4b264c5681
3 changed files with 63 additions and 23 deletions
+18 -7
View File
@@ -362,17 +362,28 @@ function BranchNotebookContent({
return (
<div className="space-y-6">
{/* Why this branch exists */}
{/* Why this branch exists — origin question is primary */}
{branchContext && (
<div className="rounded-lg border border-blue-200/60 bg-blue-50/40 px-5 py-4">
<h2 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">
Current branch
Investigating
</h2>
<p className="text-base font-medium leading-tight text-gray-900">{branchContext.label}</p>
{branchContext.origin && (
<p className="mt-2 text-sm leading-relaxed text-gray-600">
Trying to understand: {branchContext.origin}
</p>
{branchContext.originQuestion ? (
<>
<p className="text-base leading-relaxed text-gray-900">{branchContext.originQuestion.text}</p>
{branchContext.label && branchContext.originQuestion.text !== branchContext.label && (
<p className="mt-2 text-xs text-gray-500/70">Branch: {branchContext.label}</p>
)}
</>
) : branchContext.origin ? (
<>
<p className="text-base font-medium leading-tight text-gray-900">{branchContext.label}</p>
<p className="mt-2 text-sm leading-relaxed text-gray-600">
Trying to understand: {branchContext.origin}
</p>
</>
) : (
<p className="text-base font-medium leading-tight text-gray-900">{branchContext.label}</p>
)}
</div>
)}
+30 -16
View File
@@ -237,8 +237,8 @@ export function derivePrimarySurface(result, status, showExperimentView, scenari
const valid = hasValidInvestigationContext(result, status, scenario);
// RTO.28A: show branch-selection surface when investigation exists but no branch is active
if (showExperimentView && valid && !activeBranchId) return "BRANCH_SELECTION";
// RTO.28B: show question-selection surface when investigation exists but no question/branch is active
if (showExperimentView && valid && !activeBranchId) return "QUESTION_SELECTION";
if (showExperimentView && valid) return "EXPERIMENT_NOTEBOOK";
if (!showExperimentView && valid) return "NORMAL_WORKSPACE";
@@ -283,6 +283,15 @@ export default function ScenarioForm() {
// Use fixture branches as the authoritative source when available
const BRANCHES = useMemo(() => branchScoped.getBranches(), [branchScoped]);
// Track the originating question for provenance
const [originQuestion, setOriginQuestion] = useState(null);
// Extract inferred questions from the fixture (flat list for post-Analyse surface)
const inferredQuestions = useMemo(() => {
if (!branchScoped || typeof branchScoped.getAllInferredQuestions !== 'function') return [];
return branchScoped.getAllInferredQuestions();
}, [branchScoped]);
// Simulate a late semantic result arriving on Branch A ~2s after a graph loads
useEffect(() => {
if (status !== "success" && status !== "error") return;
@@ -538,7 +547,7 @@ export default function ScenarioForm() {
<>
<PulseStyle />
{!activeBranchId ? (
/* RTO.28A: no active branch — show branch-selection surface */
/* RTO.28B: inferred questions surface — user chooses what to investigate */
<div className="max-w-xl mx-auto space-y-6">
<div className="space-y-4">
{/* Situation remains visible */}
@@ -552,25 +561,30 @@ export default function ScenarioForm() {
)}
</div>
{/* Branch selection */}
{/* Inferred questions — user chooses what to investigate */}
<div className="space-y-4">
<h2 className="text-xs font-medium text-gray-700">Choose a branch to continue</h2>
<p className="text-sm text-gray-500">These are the current lines of inquiry. Pick whichever you want to work on.</p>
<h2 className="text-xs font-medium text-gray-700">Questions to explore</h2>
<p className="text-sm text-gray-500">Click a question to start investigating. A branch will be created for your choice.</p>
{BRANCHES.map((branch) => (
{inferredQuestions.map((q) => (
<button
key={branch.id}
onClick={() => setActiveBranchId(branch.id)}
key={q.id}
onClick={() => {
const targetBranch = BRANCHES.find(b => b.id === q.branchId);
if (targetBranch) {
setActiveBranchId(targetBranch.id);
setOriginQuestion({ id: q.id, text: q.text });
}
}}
className="w-full text-left rounded-lg border border-gray-200 bg-white px-5 py-4 transition hover:border-gray-300 hover:bg-gray-50"
>
<span className="block font-medium text-sm text-gray-900">{branch.label}</span>
{branch.origin && (
<span className="block mt-1 text-xs leading-relaxed text-gray-500/80">
{branch.origin}
</span>
)}
<span className="block text-sm leading-relaxed text-gray-900">{q.text}</span>
</button>
))}
{inferredQuestions.length === 0 && (
<p className="text-sm text-gray-400">No questions available yet.</p>
)}
</div>
</div>
) : (
@@ -592,7 +606,7 @@ export default function ScenarioForm() {
setAnswer={() => {}}
onAnswerSubmit={async (e) => e.preventDefault()}
lastSubmittedAnswer=""
branchContext={branchContext}
branchContext={{ ...branchContext, originQuestion: originQuestion }}
experimentalBranches={BRANCHES.length > 0 ? BRANCHES : undefined}
branchLocalQuestions={experimentalBranchQuestions.length > 0 ? experimentalBranchQuestions : undefined}
branchLocalContributions={experimentalBranchContributions.length > 0 ? experimentalBranchContributions : undefined}
+15
View File
@@ -190,6 +190,21 @@ export function useBranchScopedFixture() {
return Object.values(BRANCH_META).map(m => ({ ...m }));
},
/** Return all inferred questions across branches (flat list for post-Analyse surface). */
getAllInferredQuestions() {
const qA1 = this._qA1;
const qA2 = this._qA2;
const qB1 = this._qB1;
const qB2 = this._qB2;
if (!qA1 || !qA2 || !qB1 || !qB2) return [];
return [
{ id: qA1.id, text: qA1.text, branchId: "branch-competitor-development" },
{ id: qA2.id, text: qA2.text, branchId: "branch-competitor-development" },
{ id: qB1.id, text: qB1.text, branchId: "branch-customer-demand" },
{ id: qB2.id, text: qB2.text, branchId: "branch-customer-demand" },
];
},
/** Return all records (for verification / debugging). */
getAllRecords() {
return allRecords;