feat: make the workspace tldr first
This commit is contained in:
+116
-135
@@ -86,45 +86,21 @@ function ActivitySpinner() {
|
||||
);
|
||||
}
|
||||
|
||||
// ── Situation card ────────────────────────────────────────────
|
||||
function SituationCard({ centralStatement }) {
|
||||
if (!centralStatement) return null;
|
||||
return (
|
||||
<div className="investigation-card rounded-lg border border-gray-200 bg-white p-5">
|
||||
<h2 className="mb-2 text-sm font-semibold uppercase tracking-wide text-gray-500">
|
||||
Your situation
|
||||
</h2>
|
||||
<p className="text-base leading-relaxed text-gray-900">{centralStatement}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Current understanding card ────────────────────────────────
|
||||
function CurrentUnderstanding({ currentSummary }) {
|
||||
const summary = resolveCurrentSummary(currentSummary);
|
||||
|
||||
if (!summary) return null;
|
||||
|
||||
return (
|
||||
<div className="investigation-card rounded-lg border border-gray-200 bg-white p-5">
|
||||
<h2 className="mb-3 text-sm font-semibold uppercase tracking-wide text-gray-500">
|
||||
What we've established
|
||||
</h2>
|
||||
<p className="text-sm leading-relaxed text-gray-700">{summary}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Current investigation card (prominent) ─────────────────────
|
||||
// ── Current investigation card (prominent hero section) ──────
|
||||
function CurrentInvestigationCard({ selectedQuestion, graph }) {
|
||||
if (!selectedQuestion) return null;
|
||||
|
||||
const q = typeof selectedQuestion === "string" ? selectedQuestion : selectedQuestion.question;
|
||||
if (!q) return null;
|
||||
|
||||
const activeNode = graph?.activeUnknownNodeId
|
||||
? graph.nodes.find((n) => n.id === graph.activeUnknownNodeId)
|
||||
: null;
|
||||
// Derive meaningful context from the active node only when it adds value
|
||||
let whyMattersText = null;
|
||||
if (graph?.activeUnknownNodeId && graph.nodes) {
|
||||
const activeNode = graph.nodes.find((n) => n.id === graph.activeUnknownNodeId);
|
||||
if (activeNode?.description && activeNode.description !== activeNode.label) {
|
||||
whyMattersText = activeNode.description;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="investigation-card rounded-lg border-2 border-green-300 bg-green-50 p-6">
|
||||
@@ -132,18 +108,10 @@ function CurrentInvestigationCard({ selectedQuestion, graph }) {
|
||||
Current investigation
|
||||
</h2>
|
||||
<p className="text-xl font-semibold leading-snug text-gray-900">{q}</p>
|
||||
{activeNode?.description && activeNode.description !== activeNode.label && (
|
||||
{whyMattersText && (
|
||||
<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>
|
||||
<h3 className="text-xs font-bold uppercase tracking-wide text-green-800">Why this matters</h3>
|
||||
<p className="text-sm text-gray-700">{whyMattersText}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -166,34 +134,50 @@ function hasGenuineCompletion(graph) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Current focus card ───────────────────────────────────────
|
||||
function CurrentFocusCard({ graph }) {
|
||||
const activeNode = graph?.activeUnknownNodeId
|
||||
? graph.nodes.find((n) => n.id === graph.activeUnknownNodeId)
|
||||
: null;
|
||||
|
||||
if (!graph || !activeNode) return null;
|
||||
|
||||
// ── Completion card (terminal state when all unknowns resolved) ─
|
||||
function CompletionCard() {
|
||||
return (
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50 px-5 py-4">
|
||||
<h3 className="mb-1 text-xs font-bold uppercase tracking-wide text-gray-500">
|
||||
Current focus
|
||||
</h3>
|
||||
<p className="text-sm leading-relaxed text-gray-700">
|
||||
We are investigating one part of your situation at a time.
|
||||
{activeNode && (
|
||||
<>
|
||||
<br />
|
||||
Right now we are trying to understand{" "}
|
||||
<strong>{activeNode.label}</strong>.
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
<div className="rounded-lg border border-green-300 bg-green-50 px-5 py-6 text-center">
|
||||
<h2 className="mb-1 text-sm font-bold uppercase tracking-wide text-green-700">Investigation complete</h2>
|
||||
<p className="text-base text-gray-800">You have provided enough information. The situation has been fully investigated.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Investigation history card (readable notebook style) ───────
|
||||
// ── Evidence exhaustion card (terminal state) ─────────────────
|
||||
function EvidenceExhaustedCard({ noQuestionReason }) {
|
||||
let message = "There is not yet enough evidence to guide the next step.";
|
||||
if (noQuestionReason) {
|
||||
const reason = String(noQuestionReason);
|
||||
if (reason.toLowerCase().includes("insufficient")) {
|
||||
message = reason;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50 px-5 py-6 text-center">
|
||||
<h2 className="mb-1 text-sm font-bold uppercase tracking-wide text-gray-500">Further investigation needed</h2>
|
||||
<p className="text-base text-gray-700">{message}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Current understanding card ────────────────────────────────
|
||||
function CurrentUnderstandingCard({ currentSummary }) {
|
||||
const summary = resolveCurrentSummary(currentSummary);
|
||||
|
||||
if (!summary) return null;
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-5">
|
||||
<h2 className="mb-3 text-sm font-semibold uppercase tracking-wide text-gray-500">
|
||||
Current understanding
|
||||
</h2>
|
||||
<p className="text-sm leading-relaxed text-gray-700">{summary}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Investigation history card (readable notebook style) ──────
|
||||
function InvestigationHistoryCard({ turn }) {
|
||||
const isCollapsed = turn._collapsed;
|
||||
|
||||
@@ -249,54 +233,22 @@ function InvestigationHistory({ turns }) {
|
||||
);
|
||||
}
|
||||
|
||||
// ── Investigation complete state ───────────────────────────────
|
||||
function InvestigationCompleteMessage({ noQuestionReason }) {
|
||||
let message = "We have established enough for now.";
|
||||
if (noQuestionReason) {
|
||||
const reason = String(noQuestionReason);
|
||||
if (
|
||||
reason.toLowerCase().includes("resolved") ||
|
||||
reason.toLowerCase().includes("satisfied") ||
|
||||
reason.toLowerCase().includes("complete")
|
||||
) {
|
||||
message = "You have provided enough information. The situation has been fully investigated.";
|
||||
} else if (reason.toLowerCase().includes("insufficient")) {
|
||||
message = "There is not yet enough evidence to guide the next step. Your original situation will remain our focus when new information becomes available.";
|
||||
} else {
|
||||
message = reason;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50 px-5 py-4 text-center transition-opacity duration-300">
|
||||
<p className="text-sm text-gray-600">{message}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Loading overlay (for both start and update) ───────────────
|
||||
function LoadingOverlay({ isLoading, elapsed, currentMessage, variant }) {
|
||||
if (!isLoading) return null;
|
||||
|
||||
const messages = variant === "update" ? UPDATE_MESSAGES : INITIAL_MESSAGES;
|
||||
let statusText = messages[0].text;
|
||||
for (const m of messages) {
|
||||
if (elapsed >= m.min) statusText = m.text;
|
||||
}
|
||||
// ── Original situation (collapsed by default) ─────────────────
|
||||
function OriginalSituation({ scenario, centralStatement }) {
|
||||
if (!scenario) return null;
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-gray-200 bg-blue-50 px-5 py-6" role="status" aria-busy="true">
|
||||
<div className="flex items-center gap-3">
|
||||
<ActivitySpinner />
|
||||
<span className="text-base font-medium text-blue-900">Working through your situation</span>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-blue-700">{statusText}</p>
|
||||
<p className="mt-1 text-xs text-blue-500" aria-live="polite">
|
||||
This has been running for {elapsed}s.
|
||||
{variant === "initial" && elapsed >= 45 && (
|
||||
<span className="block mt-1">This can take around a minute with the current local model.</span>
|
||||
<details className="rounded-lg border border-gray-200 bg-gray-50">
|
||||
<summary className="cursor-pointer px-5 py-3 text-sm font-medium text-gray-600 hover:text-gray-800">
|
||||
Original situation
|
||||
</summary>
|
||||
<div className="border-t border-gray-200 px-5 pb-4 pt-3 space-y-3">
|
||||
{centralStatement && (
|
||||
<p className="text-sm text-gray-600 italic">{centralStatement}</p>
|
||||
)}
|
||||
</p>
|
||||
<p className="whitespace-pre-wrap text-sm text-gray-800">{scenario}</p>
|
||||
</div>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -384,8 +336,36 @@ function DeveloperDetails({ graph, selectedQuestion, diagnostics, newlySurfacedN
|
||||
);
|
||||
}
|
||||
|
||||
// ── Loading overlay (for both start and update) ───────────────
|
||||
function LoadingOverlay({ isLoading, elapsed, currentMessage, variant }) {
|
||||
if (!isLoading) return null;
|
||||
|
||||
const messages = variant === "update" ? UPDATE_MESSAGES : INITIAL_MESSAGES;
|
||||
let statusText = messages[0].text;
|
||||
for (const m of messages) {
|
||||
if (elapsed >= m.min) statusText = m.text;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-gray-200 bg-blue-50 px-5 py-6" role="status" aria-busy="true">
|
||||
<div className="flex items-center gap-3">
|
||||
<ActivitySpinner />
|
||||
<span className="text-base font-medium text-blue-900">Working through your situation</span>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-blue-700">{statusText}</p>
|
||||
<p className="mt-1 text-xs text-blue-500" aria-live="polite">
|
||||
This has been running for {elapsed}s.
|
||||
{variant === "initial" && elapsed >= 45 && (
|
||||
<span className="block mt-1">This can take around a minute with the current local model.</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Main workspace component ──────────────────────────────────
|
||||
export default function ReasoningWorkspace({
|
||||
scenario,
|
||||
status,
|
||||
updateStatus,
|
||||
result,
|
||||
@@ -459,25 +439,24 @@ export default function ReasoningWorkspace({
|
||||
const genuineCompletion = hasGenuineCompletion(graph);
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div className="space-y-6">
|
||||
{/* ── Loading overlays ─────────────────────────────── */}
|
||||
{status === "loading" && (
|
||||
<LoadingOverlay
|
||||
isLoading={status === "loading"}
|
||||
elapsed={startElapsed}
|
||||
currentMessage={startMsg}
|
||||
variant="initial"
|
||||
/>
|
||||
{updateStatus === "loading" && (
|
||||
<div className="h-px bg-gray-100" />
|
||||
)}
|
||||
{updateStatus === "loading" && (
|
||||
<LoadingOverlay
|
||||
isLoading={updateStatus === "loading"}
|
||||
elapsed={updateElapsed}
|
||||
currentMessage={updateMsg}
|
||||
variant="update"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* ── User-facing workspace ────────────────────────── */}
|
||||
{/* ── No graph produced after initial analysis ───────── */}
|
||||
{(status === "success" || status === "error") && !graph ? (
|
||||
<div className="rounded-lg border border-yellow-300 bg-yellow-50 px-4 py-3 text-sm text-yellow-800">
|
||||
{noQuestionReason
|
||||
@@ -486,32 +465,27 @@ export default function ReasoningWorkspace({
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Post-update acknowledgement */}
|
||||
{updateStatus === "success" && canAnswer && <UpdateAcknowledgement updateResult={result} />}
|
||||
{/* ── 1. Current investigation (or terminal state) ─ */}
|
||||
{canAnswer && <CurrentInvestigationCard selectedQuestion={selectedQ} graph={graph} />}
|
||||
|
||||
{/* Completion state */}
|
||||
{/* Terminal states occupy the hero position when no question */}
|
||||
{status === "success" && !canAnswer && graph && genuineCompletion && (
|
||||
<InvestigationCompleteMessage noQuestionReason={noQuestionReason} />
|
||||
<CompletionCard />
|
||||
)}
|
||||
{status === "success" && !canAnswer && graph && !genuineCompletion && updateStatus !== "success" && (
|
||||
<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="mt-1 text-xs text-gray-500">More evidence may be needed before a next step is clear.</p>
|
||||
</div>
|
||||
<EvidenceExhaustedCard noQuestionReason={noQuestionReason} />
|
||||
)}
|
||||
|
||||
{/* Situation context */}
|
||||
{graph && <SituationCard centralStatement={graph.centralStatement} />}
|
||||
{graph && <CurrentUnderstanding currentSummary={graph.currentSummary} />}
|
||||
{/* ── 2. Post-update acknowledgement ─────────────── */}
|
||||
{updateStatus === "success" && canAnswer && <UpdateAcknowledgement updateResult={result} />}
|
||||
|
||||
{/* ── Investigation history (below situation, above current focus) ─ */}
|
||||
{/* ── 3. Current understanding ───────────────────── */}
|
||||
{canAnswer && graph && <CurrentUnderstandingCard currentSummary={graph.currentSummary} />}
|
||||
|
||||
{/* ── 4. Investigation history ───────────────────── */}
|
||||
<InvestigationHistory turns={investigationHistory} />
|
||||
|
||||
{/* Active investigation (only when we have a question to answer) */}
|
||||
{canAnswer && <CurrentInvestigationCard selectedQuestion={selectedQ} graph={graph} />}
|
||||
{canAnswer && <CurrentFocusCard graph={graph} />}
|
||||
|
||||
{/* ── Answer form ──────────────────────────────── */}
|
||||
{/* ── 5. Response form (immediately after Current investigation) ─ */}
|
||||
{canAnswer && (
|
||||
<form onSubmit={handleUpdateCaptureAndSubmit} className="space-y-4 rounded-lg border border-gray-200 bg-white p-5">
|
||||
<div>
|
||||
@@ -543,7 +517,10 @@ export default function ReasoningWorkspace({
|
||||
</form>
|
||||
)}
|
||||
|
||||
{/* ── Developer details (collapsed by default) ─── */}
|
||||
{/* ── 6. Original situation (collapsed by default) ─ */}
|
||||
<OriginalSituation scenario={scenario} centralStatement={graph?.centralStatement} />
|
||||
|
||||
{/* ── 7. Developer details (collapsed by default) ── */}
|
||||
{(status === "success" || status === "error") && graph && (
|
||||
<DeveloperDetails
|
||||
graph={graph}
|
||||
@@ -553,6 +530,10 @@ export default function ReasoningWorkspace({
|
||||
updateResult={updateStatus === "success" ? result : null}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* ── Terminal state with understanding (when no active question) ─ */}
|
||||
{status === "success" && !canAnswer && graph && genuineCompletion && <CurrentUnderstandingCard currentSummary={graph.currentSummary} />}
|
||||
{status === "success" && !canAnswer && graph && !genuineCompletion && updateStatus !== "success" && <CurrentUnderstandingCard currentSummary={graph.currentSummary} />}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -309,6 +309,7 @@ export default function ScenarioForm() {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{status === "idle" && (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
@@ -322,7 +323,6 @@ export default function ScenarioForm() {
|
||||
<span className="text-xs text-gray-400">
|
||||
{scenario.length}/{MAX_LENGTH}
|
||||
</span>
|
||||
{status === "idle" && (
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!scenario.trim()}
|
||||
@@ -330,9 +330,9 @@ export default function ScenarioForm() {
|
||||
>
|
||||
Analyse
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{/* ── Initial analysis loading card ─────────────── */}
|
||||
{status === "loading" && (
|
||||
@@ -347,6 +347,7 @@ export default function ScenarioForm() {
|
||||
{/* ── Main result workspace ─────────────────────── */}
|
||||
{(status === "success" || status === "error" || updateStatus === "success") && (
|
||||
<ReasoningWorkspace
|
||||
scenario={scenario}
|
||||
status={status}
|
||||
updateStatus={updateStatus}
|
||||
result={{
|
||||
@@ -364,6 +365,27 @@ export default function ScenarioForm() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Reset button after successful analysis */}
|
||||
{status === "success" && (
|
||||
<div className="text-center">
|
||||
<button
|
||||
onClick={() => {
|
||||
setScenario("");
|
||||
setStatus("idle");
|
||||
setResult(null);
|
||||
setAnswer("");
|
||||
setUpdateStatus("idle");
|
||||
setUpdateResult(null);
|
||||
setLastSubmittedAnswer("");
|
||||
setUpdateError(null);
|
||||
}}
|
||||
className="rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-600 transition hover:bg-gray-50"
|
||||
>
|
||||
Start new investigation
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty state */}
|
||||
{status === "idle" && (
|
||||
<div className="rounded-lg border border-dashed border-gray-300 bg-gray-50 px-6 py-8 text-center">
|
||||
|
||||
Reference in New Issue
Block a user