feat: restructure workspace as investigation notebook
- Remove InvestigationProgress card (eliminated misleading node-count progress) - Replace with CurrentInvestigationCard showing question + 'why we are asking' + 'what we investigate' from active node context - Add CurrentFocusCard explaining what the engine is investigating and why it matters - Add InvestigationHistory section below answer form (chronological turn cards with collapsible details) - Each history card captures: question, answer, engine response, timestamp - Simplify UpdateAcknowledgement to single-line display without repeating user's answer - Remove 'remaining count' text and any graph-derived progress numbers from user-facing UI UI philosophy shift: form -> investigation workspace
This commit is contained in:
@@ -120,18 +120,36 @@ function CurrentUnderstanding({ currentSummary }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Current investigation card (prominent) ─────────────────────
|
// ── Current investigation card (prominent) ─────────────────────
|
||||||
function CurrentInvestigationCard({ selectedQuestion }) {
|
function CurrentInvestigationCard({ selectedQuestion, graph }) {
|
||||||
if (!selectedQuestion) return null;
|
if (!selectedQuestion) return null;
|
||||||
|
|
||||||
const q = typeof selectedQuestion === "string" ? selectedQuestion : selectedQuestion.question;
|
const q = typeof selectedQuestion === "string" ? selectedQuestion : selectedQuestion.question;
|
||||||
if (!q) return null;
|
if (!q) return null;
|
||||||
|
|
||||||
|
const activeNode = graph?.activeUnknownNodeId
|
||||||
|
? graph.nodes.find((n) => n.id === graph.activeUnknownNodeId)
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="investigation-card rounded-lg border-2 border-green-300 bg-green-50 p-6 transition-opacity duration-300">
|
<div className="investigation-card rounded-lg border-2 border-green-300 bg-green-50 p-6">
|
||||||
<h2 className="mb-2 text-sm font-bold uppercase tracking-wide text-green-700">
|
<h2 className="mb-2 text-sm font-bold uppercase tracking-wide text-green-700">
|
||||||
Current investigation
|
Current investigation
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-xl font-semibold leading-snug text-gray-900">{q}</p>
|
<p className="text-xl font-semibold leading-snug text-gray-900">{q}</p>
|
||||||
|
{activeNode?.description && activeNode.description !== activeNode.label && (
|
||||||
|
<div className="mt-4 space-y-1">
|
||||||
|
<h3 className="text-xs font-bold uppercase tracking-wide text-green-800">Why we are asking this</h3>
|
||||||
|
<p className="text-sm text-gray-700">{activeNode.description}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{graph?.activeUnknownNodeId && activeNode && (
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
<h3 className="text-xs font-bold uppercase tracking-wide text-green-800">What we are investigating</h3>
|
||||||
|
<p className="text-sm text-gray-700">
|
||||||
|
Understanding whether "{activeNode.label}" affects the confidence in this situation.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -152,49 +170,70 @@ function hasGenuineCompletion(graph) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Investigation progress card ──────────────────────────────
|
// ── Current focus card ───────────────────────────────────────
|
||||||
function InvestigationProgress({ graph, noQuestionReason: rwNoQuestionReason }) {
|
function CurrentFocusCard({ graph }) {
|
||||||
if (!graph?.nodes?.length) return null;
|
const activeNode = graph?.activeUnknownNodeId
|
||||||
|
|
||||||
const resolvedIds = new Set(graph.resolvedNodeIds || []);
|
|
||||||
const unknowns = graph.nodes.filter((n) => n.kind === "unknown");
|
|
||||||
const remainingCount = unknowns.filter(
|
|
||||||
(u) => u.status !== "resolved" && !resolvedIds.has(u.id),
|
|
||||||
).length;
|
|
||||||
const activeNode = graph.activeUnknownNodeId
|
|
||||||
? graph.nodes.find((n) => n.id === graph.activeUnknownNodeId)
|
? graph.nodes.find((n) => n.id === graph.activeUnknownNodeId)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const isComplete = hasGenuineCompletion(graph);
|
if (!graph || !activeNode) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="investigation-card rounded-lg border border-gray-200 bg-white px-5 py-4">
|
<div className="rounded-lg border border-gray-200 bg-gray-50 px-5 py-4">
|
||||||
{remainingCount > 0 && !isComplete ? (
|
<h3 className="mb-1 text-xs font-bold uppercase tracking-wide text-gray-500">
|
||||||
<p className="text-sm leading-relaxed text-gray-700">
|
Current focus
|
||||||
We are still building confidence about your situation.{" "}
|
</h3>
|
||||||
{remainingCount === 1
|
<p className="text-sm leading-relaxed text-gray-700">
|
||||||
? "One area remains."
|
We are investigating one part of your situation at a time.
|
||||||
: `${remainingCount} areas remain.`}
|
{activeNode && (
|
||||||
</p>
|
<>
|
||||||
) : (
|
<br />
|
||||||
<p className="text-sm leading-relaxed text-gray-700">
|
Right now we are trying to understand{" "}
|
||||||
All areas under investigation are now complete.
|
<strong>{activeNode.label}</strong>.
|
||||||
</p>
|
</>
|
||||||
)}
|
)}
|
||||||
{activeNode && (
|
</p>
|
||||||
<>
|
</div>
|
||||||
<h3 className="mt-3 mb-1 text-xs font-medium uppercase tracking-wide text-gray-400">
|
);
|
||||||
Current focus
|
}
|
||||||
</h3>
|
|
||||||
<p className="text-sm font-medium text-gray-900">{activeNode.label}</p>
|
// ── Investigation history card ────────────────────────────────
|
||||||
{activeNode.description && activeNode.description !== activeNode.label && (
|
function InvestigationHistoryCard({ turn }) {
|
||||||
<p className="mt-1 text-xs text-gray-500">Why it matters: {activeNode.description}</p>
|
return (
|
||||||
)}
|
<details className="rounded-lg border border-gray-100 bg-gray-50/60 px-4 py-3">
|
||||||
</>
|
<summary className="cursor-pointer text-xs font-semibold uppercase tracking-wide text-gray-400 hover:text-gray-600">
|
||||||
)}
|
{new Date(turn.timestamp).toLocaleString(undefined, {
|
||||||
{!activeNode && remainingCount === 0 && (
|
month: "short",
|
||||||
<p className="mt-3 text-sm text-gray-500">There is nothing further to investigate at this time.</p>
|
day: "numeric",
|
||||||
)}
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
})}
|
||||||
|
</summary>
|
||||||
|
<div className="mt-2 space-y-2 text-sm">
|
||||||
|
<p><strong>{turn.question}</strong></p>
|
||||||
|
<p className="text-gray-700">{turn.answer}</p>
|
||||||
|
{turn.engineResponse && (
|
||||||
|
<p className="italic text-gray-500">{turn.engineResponse}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Investigation history section ─────────────────────────────
|
||||||
|
function InvestigationHistory({ turns }) {
|
||||||
|
if (!turns || turns.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h2 className="text-xs font-bold uppercase tracking-wider text-gray-400">
|
||||||
|
Investigation history
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{turns.map((turn, idx) => (
|
||||||
|
<InvestigationHistoryCard key={idx} turn={turn} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -251,13 +290,12 @@ function LoadingOverlay({ isLoading, elapsed, currentMessage, variant }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Update acknowledgement ────────────────────────────────────
|
// ── Update acknowledgement ────────────────────────────────────
|
||||||
function UpdateAcknowledgement({ answer, updateResult }) {
|
function UpdateAcknowledgement({ updateResult }) {
|
||||||
if (!updateResult || !answer?.trim()) return null;
|
if (!updateResult) return null;
|
||||||
|
|
||||||
const hasResolvedNodes =
|
const summary = updateResult.summary;
|
||||||
updateResult.resolvedUnknownNodeIds && updateResult.resolvedUnknownNodeIds.length > 0;
|
const hasResolvedNodes = updateResult.resolvedUnknownNodeIds?.length > 0;
|
||||||
const hasAffectedNodes =
|
const hasAffectedNodes = updateResult.affectedNodeIds?.length > 0;
|
||||||
updateResult.affectedNodeIds && updateResult.affectedNodeIds.length > 0;
|
|
||||||
const graph = updateResult.updatedSituationGraph;
|
const graph = updateResult.updatedSituationGraph;
|
||||||
|
|
||||||
function getNodeText(nodeId) {
|
function getNodeText(nodeId) {
|
||||||
@@ -274,34 +312,24 @@ function UpdateAcknowledgement({ answer, updateResult }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let changedText;
|
let changedText;
|
||||||
if (hasResolvedNodes || hasAffectedNodes) {
|
if (hasResolvedNodes) {
|
||||||
const items = [];
|
const items = [];
|
||||||
if (hasResolvedNodes) {
|
for (const id of updateResult.resolvedUnknownNodeIds.slice(0, 3)) {
|
||||||
for (const id of updateResult.resolvedUnknownNodeIds.slice(0, 5)) {
|
items.push(getNodeText(id));
|
||||||
items.push(getNodeText(id));
|
|
||||||
}
|
|
||||||
if (updateResult.resolvedUnknownNodeIds.length > 5) {
|
|
||||||
items.push(`and ${updateResult.resolvedUnknownNodeIds.length - 5} more resolved`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (hasAffectedNodes && !hasResolvedNodes) {
|
if (updateResult.resolvedUnknownNodeIds.length > 3) {
|
||||||
for (const id of updateResult.affectedNodeIds.slice(0, 5)) {
|
items.push(`and ${updateResult.resolvedUnknownNodeIds.length - 3} more resolved`);
|
||||||
items.push(getNodeText(id));
|
|
||||||
}
|
|
||||||
if (updateResult.affectedNodeIds.length > 5) {
|
|
||||||
items.push(`and ${updateResult.affectedNodeIds.length - 5} more affected`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (items.length === 0 && updateResult.changesApplied) {
|
changedText = items.join(". ") + ".";
|
||||||
const parts = [];
|
} else if (hasAffectedNodes) {
|
||||||
const ca = updateResult.changesApplied;
|
const items = [];
|
||||||
if (ca.addedNodeCount) parts.push(`${ca.addedNodeCount} node(s) added`);
|
for (const id of updateResult.affectedNodeIds.slice(0, 3)) {
|
||||||
if (ca.updatedNodeCount) parts.push(`${ca.updatedNodeCount} node(s) updated`);
|
items.push(getNodeText(id));
|
||||||
if (ca.resolvedUnknownCount) parts.push(`${ca.resolvedUnknownCount} unknown(s) resolved`);
|
|
||||||
changedText = parts.join(", ");
|
|
||||||
} else {
|
|
||||||
changedText = items.join(". ") + ".";
|
|
||||||
}
|
}
|
||||||
|
if (updateResult.affectedNodeIds.length > 3) {
|
||||||
|
items.push(`and ${updateResult.affectedNodeIds.length - 3} more affected`);
|
||||||
|
}
|
||||||
|
changedText = items.join(". ") + ".";
|
||||||
} else if (updateResult.changesApplied) {
|
} else if (updateResult.changesApplied) {
|
||||||
const ca = updateResult.changesApplied;
|
const ca = updateResult.changesApplied;
|
||||||
const parts = [];
|
const parts = [];
|
||||||
@@ -312,23 +340,11 @@ function UpdateAcknowledgement({ answer, updateResult }) {
|
|||||||
changedText = parts.length > 0 ? parts.join(", ") : null;
|
changedText = parts.length > 0 ? parts.join(", ") : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const summary = updateResult.summary || null;
|
const displayMessage = summary || changedText || "Your answer has been added to the investigation.";
|
||||||
const displayChanged = summary || changedText || "Your answer has been added to the investigation.";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border border-blue-200 bg-blue-50 px-5 py-4 space-y-3">
|
<div className="rounded-lg border border-blue-100 bg-blue-50/80 px-5 py-3 text-sm text-blue-900">
|
||||||
<div>
|
{displayMessage}
|
||||||
<h3 className="mb-1 text-xs font-medium uppercase tracking-wide text-blue-700">
|
|
||||||
You told us
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm leading-relaxed text-blue-900">{answer}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="mb-1 text-xs font-medium uppercase tracking-wide text-blue-700">
|
|
||||||
What changed
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm leading-relaxed text-blue-900">{displayChanged}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -367,6 +383,35 @@ export default function ReasoningWorkspace({
|
|||||||
onAnswerSubmit,
|
onAnswerSubmit,
|
||||||
lastSubmittedAnswer,
|
lastSubmittedAnswer,
|
||||||
}) {
|
}) {
|
||||||
|
const [investigationHistory, setInvestigationHistory] = useState([]);
|
||||||
|
|
||||||
|
// Capture the previous question before each new question is set
|
||||||
|
const prevQuestionRef = useRef(null);
|
||||||
|
const hasCapturedInitialQuestion = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (result?.selectedQuestion && !hasCapturedInitialQuestion.current) {
|
||||||
|
prevQuestionRef.current = result.selectedQuestion;
|
||||||
|
hasCapturedInitialQuestion.current = true;
|
||||||
|
}
|
||||||
|
}, [result?.selectedQuestion]);
|
||||||
|
|
||||||
|
// Append completed turn to history after a successful update
|
||||||
|
useEffect(() => {
|
||||||
|
if (updateStatus === "success" && lastSubmittedAnswer) {
|
||||||
|
const q = prevQuestionRef.current;
|
||||||
|
setInvestigationHistory((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
question: typeof q === "string" ? q : q?.question ?? "",
|
||||||
|
answer: lastSubmittedAnswer,
|
||||||
|
engineResponse: result?.summary || null,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}, [updateStatus, lastSubmittedAnswer]);
|
||||||
|
|
||||||
const { elapsed: startElapsed, currentMessage: startMsg } = useLoadingStatus(
|
const { elapsed: startElapsed, currentMessage: startMsg } = useLoadingStatus(
|
||||||
INITIAL_MESSAGES,
|
INITIAL_MESSAGES,
|
||||||
status === "loading"
|
status === "loading"
|
||||||
@@ -392,12 +437,7 @@ export default function ReasoningWorkspace({
|
|||||||
const newlySurfacedNodeIds = result?.newlySurfacedNodeIds || [];
|
const newlySurfacedNodeIds = result?.newlySurfacedNodeIds || [];
|
||||||
const noQuestionReason = diagnostics?.noQuestionReason ?? null;
|
const noQuestionReason = diagnostics?.noQuestionReason ?? null;
|
||||||
|
|
||||||
const remainingUnknowns = graph?.nodes?.filter(
|
|
||||||
(n) => n.kind === "unknown" && n.status !== "resolved" && !(graph.resolvedNodeIds || []).includes(n.id),
|
|
||||||
);
|
|
||||||
|
|
||||||
const genuineCompletion = hasGenuineCompletion(graph);
|
const genuineCompletion = hasGenuineCompletion(graph);
|
||||||
const unresolvedRemaining = !genuineCompletion && remainingUnknowns ? remainingUnknowns.length > 0 : false;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
@@ -427,26 +467,27 @@ export default function ReasoningWorkspace({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* ── Post-update acknowledgement ─────────────── */}
|
{/* Post-update acknowledgement */}
|
||||||
{updateStatus === "success" && graph && (
|
{updateStatus === "success" && canAnswer && <UpdateAcknowledgement updateResult={result} />}
|
||||||
<UpdateAcknowledgement answer={lastSubmittedAnswer} updateResult={result} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Completion state (only when there is no next question and nothing remains) */}
|
{/* Completion state */}
|
||||||
{status === "success" && !canAnswer && graph && genuineCompletion && (
|
{status === "success" && !canAnswer && graph && genuineCompletion && (
|
||||||
<InvestigationCompleteMessage noQuestionReason={noQuestionReason} />
|
<InvestigationCompleteMessage noQuestionReason={noQuestionReason} />
|
||||||
)}
|
)}
|
||||||
{status === "success" && !canAnswer && graph && unresolvedRemaining && updateStatus !== "success" && (
|
{status === "success" && !canAnswer && graph && !genuineCompletion && updateStatus !== "success" && (
|
||||||
<div className="rounded-lg border border-gray-200 bg-gray-50 px-5 py-4 text-center">
|
<div className="rounded-lg border border-gray-200 bg-gray-50 px-5 py-4 text-center">
|
||||||
<p className="text-sm text-gray-600">There is no further question the engine can justify at the moment.</p>
|
<p className="text-sm text-gray-600">There is no further question the engine can justify at the moment.</p>
|
||||||
<p className="mt-1 text-xs text-gray-500">More evidence may be needed before a next step is clear.</p>
|
<p className="mt-1 text-xs text-gray-500">More evidence may be needed before a next step is clear.</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Situation context */}
|
||||||
{graph && <SituationCard centralStatement={graph.centralStatement} />}
|
{graph && <SituationCard centralStatement={graph.centralStatement} />}
|
||||||
{graph && <CurrentUnderstanding currentSummary={graph.currentSummary} />}
|
{graph && <CurrentUnderstanding currentSummary={graph.currentSummary} />}
|
||||||
{canAnswer && <CurrentInvestigationCard selectedQuestion={selectedQ} />}
|
|
||||||
{graph && <InvestigationProgress graph={graph} noQuestionReason={noQuestionReason} />}
|
{/* Active investigation (only when we have a question to answer) */}
|
||||||
|
{canAnswer && <CurrentInvestigationCard selectedQuestion={selectedQ} graph={graph} />}
|
||||||
|
{canAnswer && <CurrentFocusCard graph={graph} />}
|
||||||
|
|
||||||
{/* ── Answer form ──────────────────────────────── */}
|
{/* ── Answer form ──────────────────────────────── */}
|
||||||
{canAnswer && (
|
{canAnswer && (
|
||||||
@@ -480,6 +521,9 @@ export default function ReasoningWorkspace({
|
|||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* ── Investigation history (below the answer form) ─ */}
|
||||||
|
<InvestigationHistory turns={investigationHistory} />
|
||||||
|
|
||||||
{/* ── Developer details (collapsed by default) ─── */}
|
{/* ── Developer details (collapsed by default) ─── */}
|
||||||
{(status === "success" || status === "error") && graph && (
|
{(status === "success" || status === "error") && graph && (
|
||||||
<DeveloperDetails
|
<DeveloperDetails
|
||||||
|
|||||||
Reference in New Issue
Block a user