test(ui): isolate branch scoped workspace experiment
This commit is contained in:
@@ -522,6 +522,11 @@ export default function ReasoningWorkspace({
|
|||||||
lastSubmittedAnswer,
|
lastSubmittedAnswer,
|
||||||
onRestart,
|
onRestart,
|
||||||
branchContext,
|
branchContext,
|
||||||
|
// ── RTO.26B — branch-local reasoning (explicit provenance) ──
|
||||||
|
experimentalBranches,
|
||||||
|
branchLocalQuestions,
|
||||||
|
branchLocalContributions,
|
||||||
|
inactiveBranchNewResults,
|
||||||
}) {
|
}) {
|
||||||
const [investigationHistory, setInvestigationHistory] = useState([]);
|
const [investigationHistory, setInvestigationHistory] = useState([]);
|
||||||
const turnCounter = useRef(0);
|
const turnCounter = useRef(0);
|
||||||
@@ -811,7 +816,7 @@ export default function ReasoningWorkspace({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── No graph produced after initial analysis ───────── */}
|
{/* ── No graph produced after initial analysis ───────── */}
|
||||||
{(status === "success" || status === "error") && !graph ? (
|
{(status === "success" || status === "error") && !graph && !(experimentalBranches && experimentalBranches.length > 0) ? (
|
||||||
<div className="rounded-lg border border-yellow-300 bg-yellow-50 px-4 py-3 text-sm text-yellow-800">
|
<div className="rounded-lg border border-yellow-300 bg-yellow-50 px-4 py-3 text-sm text-yellow-800">
|
||||||
{diagnostics?.noQuestionReason
|
{diagnostics?.noQuestionReason
|
||||||
? "Validation failed — no structured graph output was produced."
|
? "Validation failed — no structured graph output was produced."
|
||||||
@@ -820,12 +825,21 @@ export default function ReasoningWorkspace({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* ── Workspace grid: persistent whenever a graph exists ─── */}
|
{/* ── Workspace grid: persistent whenever a graph exists ─── */}
|
||||||
{hasGraph && (
|
{(hasGraph || experimentalBranches) && (
|
||||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||||
{/* ── Left lane: active conversation & notebook ───────── */}
|
{/* ── Left lane: active conversation & notebook ───────── */}
|
||||||
<div className="space-y-6 lg:col-span-2">
|
<div className={`space-y-6 ${hasGraph ? 'lg:col-span-2' : 'lg:col-span-full'}`}>
|
||||||
|
{/* RTO.26B — experiment header when using branch-scoped fixture */}
|
||||||
|
{experimentalBranches && experimentalBranches.length > 0 && (
|
||||||
|
<div className="rounded-lg border border-purple-300/60 bg-purple-50/30 px-4 py-2">
|
||||||
|
<p className="text-[10px] font-semibold tracking-widest uppercase text-purple-400/70">
|
||||||
|
RTO.26B — branch-scoped reasoning (experimental fixture)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Active branch context (experiment RTO.25D) */}
|
{/* Active branch context (experiment RTO.25D) */}
|
||||||
{branchContext && hasGraph && (
|
{branchContext && (hasGraph || experimentalBranches) && (
|
||||||
<div className="rounded-lg border border-blue-200/60 bg-blue-50/40 px-5 py-4">
|
<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">
|
<h2 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">
|
||||||
Current branch
|
Current branch
|
||||||
@@ -841,9 +855,12 @@ export default function ReasoningWorkspace({
|
|||||||
|
|
||||||
{/* ── Open questions (case workspace) — no ranking bias ── */}
|
{/* ── Open questions (case workspace) — no ranking bias ── */}
|
||||||
{(() => {
|
{(() => {
|
||||||
const openNodes = graph.nodes.filter(
|
/* RTO.26B: when experimental branch-scoped data is present, use it as the source of truth */
|
||||||
(n) => n.kind === "unknown" && n.status !== "resolved" && !doneForNowIds.includes(n.id)
|
const openNodes = experimentalBranches && branchLocalQuestions && branchLocalQuestions.length > 0
|
||||||
);
|
? branchLocalQuestions
|
||||||
|
: ((graph?.nodes || []).filter(
|
||||||
|
(n) => n.kind === "unknown" && n.status !== "resolved" && !doneForNowIds.includes(n.id),
|
||||||
|
));
|
||||||
if (openNodes.length <= 1) return null;
|
if (openNodes.length <= 1) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -880,6 +897,23 @@ export default function ReasoningWorkspace({
|
|||||||
{/* Invitation — shows when selected but no content yet */}
|
{/* Invitation — shows when selected but no content yet */}
|
||||||
{isSelected && !hasFocusedContent() && (
|
{isSelected && !hasFocusedContent() && (
|
||||||
<div className="mt-3 space-y-3">
|
<div className="mt-3 space-y-3">
|
||||||
|
{/* Branch-local contribution scoped to this question */}
|
||||||
|
{(experimentalBranchContributions?.length ?? 0) > 0 && (
|
||||||
|
(() => {
|
||||||
|
const qContribs = experimentalBranchContributions.filter(
|
||||||
|
c => node.id === c.questionId,
|
||||||
|
);
|
||||||
|
if (!qContribs.length) return null;
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border border-gray-200/80 bg-gray-50/60 px-4 py-3">
|
||||||
|
<h3 className="mb-1 text-[10px] font-semibold tracking-widest uppercase text-gray-400">
|
||||||
|
Contribution
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm leading-relaxed text-gray-700">{qContribs[0].text}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()
|
||||||
|
)}
|
||||||
<p className="text-sm leading-relaxed text-gray-500">
|
<p className="text-sm leading-relaxed text-gray-500">
|
||||||
We have not explored this yet. Do you want to work through it?
|
We have not explored this yet. Do you want to work through it?
|
||||||
</p>
|
</p>
|
||||||
@@ -1040,28 +1074,31 @@ export default function ReasoningWorkspace({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<button
|
{/* Footer controls — horizontally separated (RTO.26B) */}
|
||||||
onClick={(e) => {
|
<div className="mt-4 mb-3 flex items-center justify-between gap-4">
|
||||||
e.stopPropagation();
|
<button
|
||||||
setFocusedPresentationItemId(null);
|
onClick={(e) => {
|
||||||
}}
|
e.stopPropagation();
|
||||||
style={{ cursor: "pointer" }}
|
setFocusedPresentationItemId(null);
|
||||||
className="text-sm text-gray-400 underline hover:text-gray-600 transition"
|
}}
|
||||||
>
|
style={{ cursor: "pointer" }}
|
||||||
Back to open questions
|
className="text-sm text-gray-400 underline hover:text-gray-600 transition whitespace-nowrap"
|
||||||
</button>
|
>
|
||||||
|
Back to open questions
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setFocusedPresentationItemId(null);
|
setFocusedPresentationItemId(null);
|
||||||
setDoneForNowIds((prev) => [...prev, node.id]);
|
setDoneForNowIds((prev) => [...prev, node.id]);
|
||||||
}}
|
}}
|
||||||
style={{ cursor: "pointer" }}
|
style={{ cursor: "pointer" }}
|
||||||
className="rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-600 hover:bg-gray-50 transition"
|
className="rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-600 hover:bg-gray-50 transition whitespace-nowrap"
|
||||||
>
|
>
|
||||||
Done for now
|
Done for now
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
@@ -1180,7 +1217,18 @@ export default function ReasoningWorkspace({
|
|||||||
{/* ── Right lane: stable supporting reference ───────── */}
|
{/* ── Right lane: stable supporting reference ───────── */}
|
||||||
{hasCurrentSummaryCondition && (
|
{hasCurrentSummaryCondition && (
|
||||||
<div className="space-y-6 lg:col-span-1">
|
<div className="space-y-6 lg:col-span-1">
|
||||||
<OriginalSituation scenario={scenario} centralStatement={graph?.centralStatement} />
|
{/* RTO.26B — situation card from scenario text when no graph */}
|
||||||
|
{!graph && scenario && (
|
||||||
|
<div className="rounded-lg border border-gray-200/60 bg-gray-50/40 px-5 py-4">
|
||||||
|
<h2 className="mb-2 text-[11px] font-semibold tracking-widest uppercase text-gray-500">
|
||||||
|
Situation
|
||||||
|
</h2>
|
||||||
|
<p className="whitespace-pre-wrap text-sm leading-relaxed text-gray-600">
|
||||||
|
{scenario}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{graph && <OriginalSituation scenario={scenario} centralStatement={graph?.centralStatement} />}
|
||||||
{/* RTO.25B — temporarily hidden to reduce competing navigation while branch-experiment is active */}
|
{/* RTO.25B — temporarily hidden to reduce competing navigation while branch-experiment is active */}
|
||||||
<div className="hidden">
|
<div className="hidden">
|
||||||
<InvestigationMap turnCount={investigationHistory.length} />
|
<InvestigationMap turnCount={investigationHistory.length} />
|
||||||
|
|||||||
+112
-18
@@ -6,6 +6,7 @@ import DiagnosticsView from "@/components/diagnostics-view";
|
|||||||
import ReasoningWorkspace, { LoadingOverlay, ContinueLaterBanner } from "@/components/reasoning-workspace";
|
import ReasoningWorkspace, { LoadingOverlay, ContinueLaterBanner } from "@/components/reasoning-workspace";
|
||||||
import ExperimentalBranchSwitcher, { PulseStyle } from "@/components/experimental/branch-switcher";
|
import ExperimentalBranchSwitcher, { PulseStyle } from "@/components/experimental/branch-switcher";
|
||||||
import { mockFetch, AVAILABLE_SCENARIOS } from "@/lib/mocks/confidence-engine/mock-client";
|
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 */
|
/* 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";
|
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) ── */
|
/* ── 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 [activeBranchId, setActiveBranchId] = useState("branch-b");
|
||||||
const [branchNewResults, setBranchNewResults] = useState({});
|
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
|
// Simulate a late semantic result arriving on Branch A ~2s after a graph loads
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status !== "success" && status !== "error") return;
|
if (status !== "success" && status !== "error") return;
|
||||||
// Only simulate once per graph load
|
|
||||||
if (branchNewResults["branch-a"]) return;
|
if (branchNewResults["branch-a"]) return;
|
||||||
|
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
@@ -252,6 +250,29 @@ export default function ScenarioForm() {
|
|||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [status, branchNewResults]);
|
}, [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) ─────────── */
|
/* Restore persisted session on mount (Phase 3) ─────────── */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
@@ -417,7 +438,75 @@ export default function ScenarioForm() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<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">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
|
||||||
{/* Two-column landing workspace */}
|
{/* 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"
|
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">
|
<label htmlFor="dismiss-facilitator" className="text-xs text-gray-500">
|
||||||
Don't show this introduction again
|
{`Dismiss this introduction permanently`}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -459,7 +548,7 @@ export default function ScenarioForm() {
|
|||||||
|
|
||||||
{/* Right panel — Workspace (2/3 on desktop) */}
|
{/* Right panel — Workspace (2/3 on desktop) */}
|
||||||
<div className={hideFacilitatorOnLanding ? "md:col-span-3" : "md:col-span-2"}>
|
<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's the situation</h2>
|
||||||
<textarea
|
<textarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
value={scenario}
|
value={scenario}
|
||||||
@@ -537,7 +626,7 @@ export default function ScenarioForm() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── Main result workspace ─────────────────────── */}
|
{/* ── Main result workspace ─────────────────────── */}
|
||||||
{((status === "success" || status === "error") && status !== "loading") && (
|
{(status === "success" || status === "error") && (
|
||||||
<>
|
<>
|
||||||
<PulseStyle />
|
<PulseStyle />
|
||||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-4">
|
<div className="grid grid-cols-1 gap-6 lg:grid-cols-4">
|
||||||
@@ -566,6 +655,11 @@ export default function ScenarioForm() {
|
|||||||
onAnswerSubmit={handleUpdate}
|
onAnswerSubmit={handleUpdate}
|
||||||
lastSubmittedAnswer={lastSubmittedAnswer}
|
lastSubmittedAnswer={lastSubmittedAnswer}
|
||||||
branchContext={branchContext}
|
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={() => {
|
onRestart={() => {
|
||||||
clearSession();
|
clearSession();
|
||||||
setStatus("idle");
|
setStatus("idle");
|
||||||
|
|||||||
Reference in New Issue
Block a user