test(ui): isolate branch scoped workspace experiment

This commit is contained in:
2026-08-20 07:35:08 +01:00
parent 47cd0c7d7b
commit 09eeed5a9e
2 changed files with 189 additions and 47 deletions
+112 -18
View File
@@ -6,6 +6,7 @@ import DiagnosticsView from "@/components/diagnostics-view";
import ReasoningWorkspace, { LoadingOverlay, ContinueLaterBanner } from "@/components/reasoning-workspace";
import ExperimentalBranchSwitcher, { PulseStyle } from "@/components/experimental/branch-switcher";
import { mockFetch, AVAILABLE_SCENARIOS } from "@/lib/mocks/confidence-engine/mock-client";
import { useBranchScopedFixture } from "@/lib/fixtures/rto26b-branch-scoped.mjs";
/* Compile-time env resolution — NEXT_PUBLIC_ vars are injected by Next.js at build */
const MOCK_ENABLED = process.env.NEXT_PUBLIC_CONFIDENCE_ENGINE_MOCKS === "true";
@@ -224,26 +225,23 @@ export default function ScenarioForm() {
/* ── RTO.25A — passive late-result branch switcher (experimental) ── */
const BRANCHES = [
{
id: "branch-a",
label: "Competitor development",
origin: "Whether competitors are developing similar products and when they might release them",
},
{
id: "branch-b",
label: "Customer demand",
origin: "How many customers will buy the product, and what revenue that represents",
},
];
const [activeBranchId, setActiveBranchId] = useState("branch-b");
const [branchNewResults, setBranchNewResults] = useState({});
/* ── RTO.26B — experimental branch-scoped reasoning fixture ───── */
const branchScoped = useBranchScopedFixture();
// Toggle to control when the experiment view is visible vs production
const [showExperimentView, setShowExperimentView] = useState(
typeof window !== "undefined" ? (window.sessionStorage?.getItem("ce-show-experiment") !== "false") : true,
);
// Use fixture branches as the authoritative source when available
const BRANCHES = useMemo(() => branchScoped.getBranches(), [branchScoped]);
// Simulate a late semantic result arriving on Branch A ~2s after a graph loads
useEffect(() => {
if (status !== "success" && status !== "error") return;
// Only simulate once per graph load
if (branchNewResults["branch-a"]) return;
const timer = setTimeout(() => {
@@ -252,6 +250,29 @@ export default function ScenarioForm() {
return () => clearTimeout(timer);
}, [status, branchNewResults]);
// Compute branch-local data for the active branch (explicit provenance)
const activeBranch = BRANCHES.find(b => b.id === activeBranchId);
const experimentalBranchQuestions = useMemo(() => {
if (!activeBranch) return [];
return branchScoped.getBranchQuestions(activeBranch.id);
}, [activeBranch, branchScoped]);
const experimentalBranchContributions = useMemo(() => {
if (!activeBranch) return [];
return branchScoped.getBranchContributions(activeBranch.id);
}, [activeBranch, branchScoped]);
// Determine which non-active branches have new results for passive indicator
const inactiveBranchNewResults = useMemo(() => {
const result = {};
BRANCHES.forEach(b => {
if (b.id !== activeBranchId && b.id in branchNewResults) {
result[b.id] = true;
}
});
return result;
}, [activeBranchId, BRANCHES, branchNewResults]);
/* Restore persisted session on mount (Phase 3) ─────────── */
useEffect(() => {
if (typeof window === "undefined") return;
@@ -417,7 +438,75 @@ export default function ScenarioForm() {
return (
<div className="space-y-6">
{status === "idle" && (
{/* ── RTO.26B — standalone branch-scoped experimental view (shown when experiment is active) ───── */}
{showExperimentView && status !== "loading" && !result?.situationGraph && !result?.updatedSituationGraph && (
<>
<PulseStyle />
<div className="grid grid-cols-1 gap-6 lg:grid-cols-4">
{/* Workspace (3/4) — branch-scoped fixture only, no API needed */}
<div className="lg:col-span-3">
{(() => {
const activeBranch = BRANCHES.find(b => b.id === activeBranchId);
const branchContext = activeBranch ? { label: activeBranch.label, origin: activeBranch.origin } : null;
return (
<ReasoningWorkspace
scenario={scenario}
status={status}
updateStatus="idle"
currentUnderstanding={currentUnderstanding}
result={{ situationGraph: null, selectedQuestion: null, newlySurfacedNodeIds: [], diagnostics: null }}
answer=""
setAnswer={() => {}}
onAnswerSubmit={async (e) => e.preventDefault()}
lastSubmittedAnswer=""
branchContext={branchContext}
experimentalBranches={BRANCHES.length > 0 ? BRANCHES : undefined}
branchLocalQuestions={experimentalBranchQuestions.length > 0 ? experimentalBranchQuestions : undefined}
branchLocalContributions={experimentalBranchContributions.length > 0 ? experimentalBranchContributions : undefined}
inactiveBranchNewResults={Object.keys(inactiveBranchNewResults).length > 0 ? inactiveBranchNewResults : undefined}
onRestart={() => { setStatus("idle"); setResult(null); setScenario(""); }}
/>
);
})()}
</div>
{/* Branch switcher (1/4 sidebar) */}
<div className="lg:col-span-1">
<ExperimentalBranchSwitcher
branches={BRANCHES}
activeBranchId={activeBranchId}
branchNewResults={branchNewResults}
onBranchSelect={(id) => {
if (id !== activeBranchId) {
setActiveBranchId(id);
}
}}
/>
</div>
</div>
</>
)}
{/* ── Experiment toggle (visible when experiment is NOT shown) ─ */}
{!showExperimentView && status !== "loading" && !result?.updatedSituationGraph && (
<div className="rounded-lg border border-gray-200/60 bg-gray-50/30 px-4 py-3 text-center">
<p className="text-sm text-gray-500 mb-3">
Production view active.
</p>
<button
onClick={() => {
setShowExperimentView(true);
try { window.sessionStorage?.setItem("ce-show-experiment", "true"); } catch {}
}}
className="rounded-lg border border-blue-600 bg-white px-4 py-2 text-sm font-medium text-blue-700 hover:bg-blue-50 transition"
>
Try branch-scoped experiment (RTO.26B)
</button>
</div>
)}
{/* ── Idle form for scenario input (shown only when experiment is off) ─ */}
{!showExperimentView && !result?.situationGraph && status === "idle" && (
<form onSubmit={handleSubmit} className="space-y-6">
{/* Two-column landing workspace */}
@@ -450,7 +539,7 @@ export default function ScenarioForm() {
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<label htmlFor="dismiss-facilitator" className="text-xs text-gray-500">
Don't show this introduction again
{`Dismiss this introduction permanently`}
</label>
</div>
</div>
@@ -459,7 +548,7 @@ export default function ScenarioForm() {
{/* Right panel — Workspace (2/3 on desktop) */}
<div className={hideFacilitatorOnLanding ? "md:col-span-3" : "md:col-span-2"}>
<h2 className="mb-4 text-xs font-bold tracking-widest uppercase text-gray-400">Tell me what's happening</h2>
<h2 className="mb-4 text-xs font-bold tracking-widest uppercase text-gray-400">What&#39;s the situation</h2>
<textarea
ref={textareaRef}
value={scenario}
@@ -537,7 +626,7 @@ export default function ScenarioForm() {
)}
{/* ── Main result workspace ─────────────────────── */}
{((status === "success" || status === "error") && status !== "loading") && (
{(status === "success" || status === "error") && (
<>
<PulseStyle />
<div className="grid grid-cols-1 gap-6 lg:grid-cols-4">
@@ -566,6 +655,11 @@ export default function ScenarioForm() {
onAnswerSubmit={handleUpdate}
lastSubmittedAnswer={lastSubmittedAnswer}
branchContext={branchContext}
// RTO.26B branch-local reasoning from explicit provenance
experimentalBranches={BRANCHES.length > 0 ? BRANCHES : undefined}
branchLocalQuestions={experimentalBranchQuestions.length > 0 ? experimentalBranchQuestions : undefined}
branchLocalContributions={experimentalBranchContributions.length > 0 ? experimentalBranchContributions : undefined}
inactiveBranchNewResults={Object.keys(inactiveBranchNewResults).length > 0 ? inactiveBranchNewResults : undefined}
onRestart={() => {
clearSession();
setStatus("idle");