feat: connect UI to situation graph start flow
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import React from "react";
|
||||
|
||||
const ValidationIndicator = ({ status }) => {
|
||||
const styles = {
|
||||
valid: "text-green-600",
|
||||
@@ -27,23 +29,53 @@ const validationIcons = {
|
||||
export default function DiagnosticsView({ result }) {
|
||||
if (!result) return null;
|
||||
|
||||
const diagnostics = result.diagnostics || result;
|
||||
|
||||
const metrics = [
|
||||
{ label: "Model", value: result.modelName || "?" },
|
||||
{ label: "Model", value: diagnostics.modelName || result.modelName || "?" },
|
||||
{ label: "Provider", value: "Ollama" },
|
||||
{ label: "Prompt version", value: result.promptVersion || "?" },
|
||||
{
|
||||
label: "Prompt version",
|
||||
value: diagnostics.promptVersion || result.promptVersion || "?",
|
||||
},
|
||||
{
|
||||
label: "Duration",
|
||||
value:
|
||||
result.responseDurationMs != null
|
||||
? `${result.responseDurationMs}ms`
|
||||
diagnostics.responseDurationMs != null
|
||||
? `${diagnostics.responseDurationMs}ms`
|
||||
: "?",
|
||||
},
|
||||
{
|
||||
label: "Validation",
|
||||
value: (
|
||||
<ValidationIndicator status={result.validationStatus || "invalid"} />
|
||||
<ValidationIndicator
|
||||
status={diagnostics.validationStatus || result.validationStatus || "invalid"}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Node count",
|
||||
value: diagnostics.nodeCount != null ? diagnostics.nodeCount : "?",
|
||||
},
|
||||
{
|
||||
label: "Edge count",
|
||||
value: diagnostics.edgeCount != null ? diagnostics.edgeCount : "?",
|
||||
},
|
||||
{
|
||||
label: "Graph references",
|
||||
value:
|
||||
diagnostics.graphReferenceValidation == null
|
||||
? "?"
|
||||
: diagnostics.graphReferenceValidation.valid
|
||||
? `${validationIcons.valid} valid`
|
||||
: `${validationIcons.invalid} invalid`,
|
||||
},
|
||||
];
|
||||
|
||||
const errors = [
|
||||
...(result.errors || []),
|
||||
...(result.validationErrors || []),
|
||||
...(result.analysisErrors || []),
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -72,14 +104,14 @@ export default function DiagnosticsView({ result }) {
|
||||
)}
|
||||
|
||||
{/* Errors if present */}
|
||||
{result.errors && result.errors.length > 0 && (
|
||||
{errors.length > 0 && (
|
||||
<details className="mt-3">
|
||||
<summary className="cursor-pointer text-xs text-red-500 underline hover:text-red-700">
|
||||
Validation errors ({result.errors.length})
|
||||
Validation errors ({errors.length})
|
||||
</summary>
|
||||
<ul className="mt-1 space-y-0.5 text-xs text-red-600">
|
||||
{result.errors.map((err, i) => (
|
||||
<li key={i}>{err}</li>
|
||||
{errors.map((err, i) => (
|
||||
<li key={i}>{typeof err === "string" ? err : err?.message || JSON.stringify(err)}</li>
|
||||
))}
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
@@ -1,14 +1,59 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { useState, useRef } from "react";
|
||||
import ReconstructionView from "@/components/reconstruction-view";
|
||||
import DiagnosticsView from "@/components/diagnostics-view";
|
||||
import SituationGraphView from "@/components/situation-graph-view";
|
||||
|
||||
const MAX_LENGTH = 10000;
|
||||
|
||||
export async function submitScenarioForStartCase(fetchImpl, scenario) {
|
||||
return fetchImpl("/api/cases/start", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ scenario }),
|
||||
});
|
||||
}
|
||||
|
||||
export function ScenarioResultPanels({ status, result }) {
|
||||
if (!result) return null;
|
||||
|
||||
const hasGraph = Boolean(result.situationGraph);
|
||||
const hasQuestion = Boolean(result.selectedQuestion?.question);
|
||||
const hasDiagnostics = Boolean(result.diagnostics);
|
||||
|
||||
return (
|
||||
<>
|
||||
{status === "error" && (
|
||||
<div className="space-y-3">
|
||||
{result.error && (
|
||||
<div className="rounded-lg border border-red-300 bg-red-50 px-4 py-3 text-sm text-red-700 whitespace-pre-wrap">
|
||||
Error: {result.error}
|
||||
</div>
|
||||
)}
|
||||
{!hasGraph && !hasQuestion && (
|
||||
<div className="rounded-lg border border-yellow-300 bg-yellow-50 px-4 py-2 text-sm text-yellow-800">
|
||||
Validation failed — no structured graph output was produced.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(status === "success" || hasGraph || hasQuestion) && (
|
||||
<SituationGraphView
|
||||
situationGraph={result.situationGraph}
|
||||
selectedQuestion={result.selectedQuestion}
|
||||
/>
|
||||
)}
|
||||
|
||||
{hasDiagnostics && <DiagnosticsView result={result} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ScenarioForm() {
|
||||
const [scenario, setScenario] = useState("");
|
||||
const [status, setStatus] = useState("idle"); // idle | loading | error | success | partial
|
||||
const [status, setStatus] = useState("idle"); // idle | loading | error | success
|
||||
const [result, setResult] = useState(null);
|
||||
const textareaRef = useRef(null);
|
||||
|
||||
@@ -18,19 +63,11 @@ export default function ScenarioForm() {
|
||||
setResult(null);
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/analyse", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ scenario }),
|
||||
});
|
||||
const res = await submitScenarioForStartCase(fetch, scenario);
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok && data.validationStatus === "valid") {
|
||||
setStatus("success");
|
||||
setResult(data);
|
||||
} else if (data.success) {
|
||||
// Success in analysis but validation may be partial
|
||||
if (res.ok && data.success) {
|
||||
setStatus("success");
|
||||
setResult(data);
|
||||
} else {
|
||||
@@ -43,14 +80,6 @@ export default function ScenarioForm() {
|
||||
}
|
||||
};
|
||||
|
||||
// Determine if we have meaningful content to display
|
||||
const hasClassification = result?.inputClassification;
|
||||
const hasReconstruction = result?.reconstruction;
|
||||
const hasNextQuestion = result?.nextQuestion;
|
||||
const hasEvidence = result?.evidence && result.evidence.length > 0;
|
||||
const hasMeaningfulContent =
|
||||
hasClassification || hasReconstruction || hasNextQuestion || hasEvidence;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
@@ -76,38 +105,7 @@ export default function ScenarioForm() {
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Error state */}
|
||||
{status === "error" && (
|
||||
<div className="space-y-3">
|
||||
{result?.error && (
|
||||
<div className="rounded-lg border border-red-300 bg-red-50 px-4 py-3 text-sm text-red-700 whitespace-pre-wrap">
|
||||
Error: {result.error}
|
||||
</div>
|
||||
)}
|
||||
{/* Show partial content even on validation failure */}
|
||||
{(hasClassification || hasReconstruction) && (
|
||||
<div className="rounded-lg border border-yellow-300 bg-yellow-50 px-4 py-2 text-sm text-yellow-800">
|
||||
⚠ Partial result — some fields failed validation. Showing what was
|
||||
accepted.
|
||||
</div>
|
||||
)}
|
||||
{hasReconstruction && (
|
||||
<ReconstructionView reconstruction={result} partial />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Success state */}
|
||||
{status === "success" && hasMeaningfulContent && (
|
||||
<div className="space-y-4">
|
||||
<ReconstructionView reconstruction={result} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Always show diagnostics when we have any result */}
|
||||
{(hasClassification || hasReconstruction || hasNextQuestion) && (
|
||||
<DiagnosticsView result={result} />
|
||||
)}
|
||||
<ScenarioResultPanels status={status} result={result} />
|
||||
|
||||
{status === "loading" && (
|
||||
<div className="py-12 text-center text-sm text-gray-400">
|
||||
@@ -123,13 +121,6 @@ export default function ScenarioForm() {
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Invalid result with no partial data */}
|
||||
{status === "error" && !result?.error && !hasMeaningfulContent && (
|
||||
<div className="rounded-lg border border-yellow-300 bg-yellow-50 px-4 py-2 text-sm text-yellow-800">
|
||||
Validation failed — no structured output was produced.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
|
||||
function NodeBadge({ children, tone = "gray" }) {
|
||||
const tones = {
|
||||
gray: "border-gray-200 bg-gray-50 text-gray-700",
|
||||
blue: "border-blue-200 bg-blue-50 text-blue-700",
|
||||
green: "border-green-200 bg-green-50 text-green-700",
|
||||
yellow: "border-yellow-200 bg-yellow-50 text-yellow-700",
|
||||
};
|
||||
|
||||
return (
|
||||
<span className={`rounded-full border px-2 py-0.5 text-xs ${tones[tone] || tones.gray}`}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function NodeGroup({ title, nodes }) {
|
||||
if (!nodes?.length) return null;
|
||||
|
||||
return (
|
||||
<section className="rounded-lg border border-gray-200 bg-white p-4">
|
||||
<h3 className="mb-3 text-sm font-semibold text-gray-700">
|
||||
{title} ({nodes.length})
|
||||
</h3>
|
||||
<ul className="space-y-3">
|
||||
{nodes.map((node) => (
|
||||
<li key={node.id} className="rounded border border-gray-100 bg-gray-50 p-3 text-sm">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="font-medium text-gray-900">{node.label}</span>
|
||||
<NodeBadge tone="blue">{node.status}</NodeBadge>
|
||||
<NodeBadge tone="green">{node.confidence}</NodeBadge>
|
||||
{node.value != null && (
|
||||
<NodeBadge tone="yellow">
|
||||
{node.value}
|
||||
{node.unit ? ` ${node.unit}` : ""}
|
||||
</NodeBadge>
|
||||
)}
|
||||
</div>
|
||||
{node.description && node.description !== node.label && (
|
||||
<p className="mt-1 text-gray-600">{node.description}</p>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SituationGraphView({
|
||||
situationGraph,
|
||||
selectedQuestion,
|
||||
}) {
|
||||
if (!situationGraph) return null;
|
||||
|
||||
const activeUnknown = situationGraph.activeUnknownNodeId
|
||||
? situationGraph.nodes.find((node) => node.id === situationGraph.activeUnknownNodeId)
|
||||
: null;
|
||||
|
||||
const nodesByKind = situationGraph.nodes.reduce((acc, node) => {
|
||||
if (!acc[node.kind]) acc[node.kind] = [];
|
||||
acc[node.kind].push(node);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{selectedQuestion?.question && (
|
||||
<section className="rounded-lg border-2 border-green-300 bg-green-50 p-5">
|
||||
<h2 className="mb-2 text-base font-bold text-green-800">Selected Question</h2>
|
||||
<p className="text-base font-medium text-gray-900">{selectedQuestion.question}</p>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="rounded-lg border border-gray-200 bg-white p-4">
|
||||
<h2 className="mb-2 text-base font-semibold text-gray-900">Situation Graph</h2>
|
||||
<dl className="space-y-2 text-sm">
|
||||
<div>
|
||||
<dt className="text-gray-500">Central statement</dt>
|
||||
<dd className="font-medium text-gray-900">{situationGraph.centralStatement}</dd>
|
||||
</div>
|
||||
{situationGraph.currentSummary && (
|
||||
<div>
|
||||
<dt className="text-gray-500">Current summary</dt>
|
||||
<dd className="text-gray-800">{situationGraph.currentSummary}</dd>
|
||||
</div>
|
||||
)}
|
||||
{activeUnknown && (
|
||||
<div>
|
||||
<dt className="text-gray-500">Active unknown</dt>
|
||||
<dd className="text-gray-900">{activeUnknown.label}</dd>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<dt className="text-gray-500">Edge count</dt>
|
||||
<dd className="text-gray-900">{situationGraph.edges.length}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
{Object.entries(nodesByKind).map(([kind, nodes]) => (
|
||||
<NodeGroup
|
||||
key={kind}
|
||||
title={kind.replace(/_/g, " ")}
|
||||
nodes={nodes}
|
||||
/>
|
||||
))}
|
||||
|
||||
<details className="rounded-lg border border-gray-200 bg-gray-50 p-4">
|
||||
<summary className="cursor-pointer text-sm font-medium text-gray-700 underline">
|
||||
Raw graph JSON
|
||||
</summary>
|
||||
<pre className="mt-3 overflow-auto rounded bg-gray-900 p-3 text-xs text-green-400">
|
||||
{JSON.stringify(situationGraph, null, 2)}
|
||||
</pre>
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user