Files
confidence-engine/components/reconstruction-view.jsx
T
robbond 956fc2e31e fix: resolve 500 errors from model returning trivial status objects (root cause + v0.2 prompt fix)
Two bugs were causing the model to return {"status":"ok"} / {"status":"ready"}
instead of structured reconstruction data, resulting in POST /api/analyse 500:

1. DOUBLE-WRAPPING BUG (lib/llm/provider.js):
   generateReconstruction() called buildPrompt(scenario) on input that was
   already a fully-built prompt string from analyseScenario(). This wrapped the
   v0.1 prompt (~5000+ chars) in another template layer, producing incomprehensible
   output that the model could not parse as structured JSON.
   Fix: Pass scenario through directly (it is ALREADY a built prompt).

2. MISSING JSON SPEC (prompts/reconstruct-v0.2.md):
   The v0.2 prompt template said 'matching the structure exactly' but never
   defined what that structure was. The model invented its own field names
   (input_classification, reasoning_mode, anchors) with snake_case instead of
   camelCase, which failed Zod validation -> 500 errors.
   Fix: Added explicit JSON schema section with exact key names, enum values,
   and nested structure matching the Zod validation layer.

Additionally:
- Refactored route to use analyseScenario from lib/analysis (centralized)
- Added lib/analysis.js with shared analysis logic
- Updated components to display promptVersion and validation errors
- Added lib/reconstruction/prompt.js v0.1/v0.2 versioning
- Added lib/reconstruction/schema.js v0.2 Zod schemas
- Added debug tool scripts, evaluation results, and comparison findings
2026-08-01 08:57:28 +01:00

301 lines
13 KiB
React

"use client";
import { useMemo } from "react";
// ── Confidence badge (shared) ────────────────────────
const confidenceColor = {
low: "text-red-600 bg-red-50 border-red-200",
medium: "text-yellow-700 bg-yellow-50 border-yellow-200",
high: "text-green-700 bg-green-50 border-green-200",
};
const ConfidenceBadge = ({ level }) => (
<span className={`inline-block rounded-full border px-2 py-0.5 text-xs font-medium ${confidenceColor[level] || "text-gray-600 bg-gray-100"}`}>
{level}
</span>
);
// ── Evidence type labels (shared) ───────────────────
const evidenceTypeLabels = {
direct_observation: "Direct Observation",
reported_statement: "Reported Statement",
interpretation: "Interpretation",
assumption: "Assumption",
inferred_relationship: "Inferred Relationship",
};
const importanceColors = {
incidental: "text-gray-500 bg-gray-50 border-gray-200",
supporting: "text-blue-700 bg-blue-50 border-blue-200",
important: "text-orange-700 bg-orange-50 border-orange-200",
critical: "text-red-800 bg-red-50 border-red-300 font-semibold",
};
const importanceLabels = {
incidental: "Incidental",
supporting: "Supporting",
important: "Important",
critical: "Critical",
};
// ── Input classification display ────────────────────
function ClassificationDisplay({ classification }) {
if (!classification) return null;
const p = classification.primaryType || classification.primary_type;
const sec = classification.secondaryTypes || classification.secondary_types || [];
const modes = classification.reasoningModes || classification.reasoning_modes || [];
// Normalize camelCase to snake_case for display if needed
const primaryLabel = String(p).replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
const secLabels = sec.map((s) => s.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()));
const modeLabels = modes.map((m) => m.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()));
return (
<div className="rounded-lg border border-blue-200 bg-blue-50 p-4">
<h3 className="mb-2 text-sm font-semibold text-blue-700">Input Classification</h3>
<dl className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1.5 text-sm">
<dt className="text-blue-500">Primary type</dt>
<dd className="font-medium">{primaryLabel}</dd>
{secLabels.length > 0 && (
<>
<dt className="text-blue-500 pt-1">Secondary types</dt>
<dd>{secLabels.join(" · ")}</dd>
</>
)}
{modeLabels.length > 0 && (
<>
<dt className="text-blue-500 pt-1">Reasoning modes</dt>
<dd>{modeLabels.join(" · ")}</dd>
</>
)}
<dt className="text-blue-500 pt-1">Classification reason</dt>
<dd className="italic">{classification.classificationReason || classification.classification_reason}</dd>
<dt className="text-blue-500 pt-1">Confidence</dt>
<dd><ConfidenceBadge level={classification.confidence} /></dd>
</dl>
</div>
);
}
// ── Reconstruction summary ──────────────────────────
function SummaryDisplay({ reconstruction }) {
if (!reconstruction?.summary) return null;
const summary = reconstruction.summary || reconstruction.Summary;
return (
<div className="rounded-lg border border-gray-200 bg-white p-4">
<h3 className="mb-2 text-sm font-semibold text-gray-600">Reconstruction Summary</h3>
<p className="text-sm leading-relaxed">{summary}</p>
</div>
);
}
// ── Generic item list (used for multiple sections) ──
function ItemList({ title, items, renderExtra }) {
const count = items?.length;
if (!count) return null; // hide empty sections entirely
const itemsArr = Array.isArray(items) ? items : [items];
return (
<div className="mb-4 rounded-lg border border-gray-200 bg-white p-4">
<h3 className="mb-2 text-sm font-semibold text-gray-600">{title} ({count})</h3>
<ul className="space-y-2">
{itemsArr.map((item, idx) => (
<li key={item.id || `${title}-${idx}`} className="rounded border border-gray-200 bg-white px-3 py-2 text-sm">
<div className="flex items-center gap-2">
{item.id && <span className="font-mono text-xs text-gray-400">#{item.id}</span>}
{item.confidence && <ConfidenceBadge level={item.confidence} />}
{item.importance && (
<span className={`inline-block rounded-full border px-2 py-0.5 text-xs font-medium ${importanceColors[item.importance] || "text-gray-600 bg-gray-100"}`}>
{importanceLabels[item.importance]}
</span>
)}
</div>
<p className="mt-1">{item.description}</p>
{renderExtra && renderExtra(item)}
</li>
))}
</ul>
</div>
);
}
// ── Plausible interpretations ───────────────────────
function InterpretationsDisplay({ interpretations }) {
if (!interpretations?.length) return null;
const arr = Array.isArray(interpretations) ? interpretations : [interpretations];
return (
<div className="mb-4 rounded-lg border border-indigo-200 bg-indigo-50 p-4">
<h3 className="mb-2 text-sm font-semibold text-indigo-700">Plausible Interpretations ({arr.length})</h3>
<ul className="space-y-3">
{arr.map((interp, idx) => (
<li key={interp.id || `${idx}`} className="rounded border border-indigo-200 bg-white px-3 py-2.5 text-sm leading-relaxed">
<div className="flex items-center gap-2 mb-1">
<span className="font-medium text-indigo-600">{interp.description}</span>
{interp.confidence && <ConfidenceBadge level={interp.confidence} />}
</div>
{interp.supportingEvidenceIds?.length > 0 && (
<p className="text-xs text-gray-500">Supporting evidence: {interp.supportingEvidenceIds.join(", ")}</p>
)}
{interp.assumptionsRequired?.length > 0 && (
<p className="text-xs italic text-gray-500">Requires assumptions: {interp.assumptionsRequired.join("; ")}</p>
)}
</li>
))}
</ul>
</div>
);
}
// ── Next question (prominent) ───────────────────────
function NextQuestionDisplay({ question }) {
if (!question?.question) return null;
const q = question.question || question.Question;
const targets = question.targets || question.Targets || [];
const reason = question.reason || question.Reason || "";
const value = question.expectedInformationValue || question.expected_information_value || "medium";
const valueLabel = { low: "Low", medium: "Medium", high: "High" }[value] || "Medium";
const valueColor = { low: "bg-yellow-100 text-yellow-800", medium: "bg-blue-100 text-blue-800", high: "bg-green-100 text-green-800" }[value] || "";
return (
<div className="rounded-lg border-2 border-green-300 bg-green-50 p-5">
<div className="flex items-center gap-2 mb-2">
<h3 className="text-sm font-bold text-green-800">Next Question</h3>
<span className={`rounded-full px-2 py-0.5 text-xs font-medium ${valueColor}`}>{valueLabel} value</span>
</div>
<p className="mb-2 text-base font-medium text-gray-900">{q}</p>
{targets.length > 0 && (
<p className="text-sm text-gray-600">Targets: {targets.join(", ")}</p>
)}
{reason && <p className="text-sm italic text-gray-500">Because: {reason}</p>}
</div>
);
}
// ── Evidence list ───────────────────────────────────
function EvidenceDisplay({ evidence }) {
if (!evidence?.length) return null;
const arr = Array.isArray(evidence) ? evidence : [evidence];
const evidenceLabels = {
direct_observation: "👁 Direct Observation",
reported_statement: "🗣 Reported Statement",
interpretation: "💡 Interpretation",
assumption: "❓ Assumption",
inferred_relationship: "🔗 Inferred Relationship",
};
return (
<div className="mb-4 rounded-lg border border-gray-200 bg-white p-4">
<h3 className="mb-2 text-sm font-semibold text-gray-600">Supporting Evidence ({arr.length})</h3>
<ul className="space-y-2">
{arr.map((item, idx) => (
<li key={item.id || `${idx}`} className="rounded border border-gray-200 bg-white px-3 py-2 text-sm leading-relaxed">
<div className="flex items-center gap-2 mb-0.5 flex-wrap">
{item.id && <span className="font-mono text-xs text-gray-400">#{item.id}</span>}
<span className={`inline-block rounded px-1.5 py-0.5 text-[10px] font-medium ${importanceColors[item.importance] || "text-gray-600 bg-gray-100"}`}>
{importanceLabels[item.importance]}
</span>
<span className="inline-block rounded px-1.5 py-0.5 text-[10px] font-medium bg-gray-100 text-gray-700">
{evidenceLabels[item.evidenceType] || item.evidenceType}
</span>
{item.confidence && <ConfidenceBadge level={item.confidence} />}
</div>
<p className="text-sm">{item.description}</p>
{(item.source || item.attribution) && (
<p className="mt-0.5 text-xs text-gray-400">Source: {item.source || item.attribution}</p>
)}
</li>
))}
</ul>
</div>
);
}
// ── Main component ──────────────────────────────────
export default function ReconstructionView({ reconstruction, partial }) {
// Handle both v0.2 direct object and wrapped result formats
const data = reconstruction;
if (partial) {
return (
<div className="rounded-lg border border-yellow-300 bg-yellow-50 px-4 py-3 text-sm text-yellow-800">
Partial result some fields failed validation. Showing what was accepted.
</div>
);
}
return (
<div className="space-y-4">
{/* Classification first */}
{data.inputClassification && (
<ClassificationDisplay classification={data.inputClassification} />
)}
{/* Summary */}
{data.reconstruction?.summary && (
<SummaryDisplay reconstruction={data.reconstruction} />
)}
{/* Key differences */}
{data.reconstruction?.differences && (
<ItemList title="Key Differences" items={data.reconstruction.differences} />
)}
{/* Unexplained transitions */}
{data.reconstruction?.unexplainedTransitions && data.reconstruction.unexplainedTransitions.length > 0 && (
<ItemList title="Unexplained Transitions" items={data.reconstruction.unexplainedTransitions} renderExtra={(i) => (
i.entity && <p className="mt-1 text-xs text-gray-500">Entity: {i.entity}</p>
)} />
)}
{/* Contradictions */}
{data.reconstruction?.contradictions && data.reconstruction.contradictions.length > 0 && (
<ItemList title="Contradictions" items={data.reconstruction.contradictions} />
)}
{/* Important unknowns */}
{data.reconstruction?.importantUnknowns && data.reconstruction.importantUnknowns.length > 0 && (
<ItemList title="Important Unknowns" items={data.reconstruction.importantUnknowns} />
)}
{/* Plausible interpretations */}
{data.reconstruction?.plausibleInterpretations && data.reconstruction.plausibleInterpretations.length > 0 && (
<InterpretationsDisplay interpretations={data.reconstruction.plausibleInterpretations} />
)}
{/* Secondary reconstruction categories (actors, systems, etc.) */}
{data.reconstruction?.actors && data.reconstruction.actors.length > 0 && (
<ItemList title="Actors" items={data.reconstruction.actors} />
)}
{data.reconstruction?.systemsOrObjects && data.reconstruction.systemsOrObjects.length > 0 && (
<ItemList title="Systems / Objects" items={data.reconstruction.systemsOrObjects} />
)}
{data.reconstruction?.expectedStates && data.reconstruction.expectedStates.length > 0 && (
<ItemList title="Expected States" items={data.reconstruction.expectedStates} />
)}
{data.reconstruction?.observedStates && data.reconstruction.observedStates.length > 0 && (
<ItemList title="Observed States" items={data.reconstruction.observedStates} />
)}
{data.reconstruction?.knownTransitions && data.reconstruction.knownTransitions.length > 0 && (
<ItemList title="Known Transitions" items={data.reconstruction.knownTransitions} renderExtra={(i) => (
<div className="mt-1 text-xs text-gray-500">
{i.entity && <span>Entity: {i.entity} · </span>}
From "{i.previousState}" To "{i.currentState}" ({i.explanationStatus})
</div>
)} />
)}
{/* Next question — prominent */}
<NextQuestionDisplay question={data.nextQuestion} />
{/* Evidence */}
{data.evidence && (
<EvidenceDisplay evidence={data.evidence} />
)}
</div>
);
}