import React, { useState } from "react"; import { ChevronRight, RotateCcw, CheckCircle2, AlertTriangle, Info, } from "lucide-react"; export interface TreeNode { id: string; question?: string; hint?: string; yesLabel?: string; noLabel?: string; yes?: TreeNode; no?: TreeNode; result?: string; resultType?: "correct" | "warning" | "info"; ruleRef?: string; } export interface TreeScenario { label: string; // Short tab label, e.g. "Sentence 1" sentence: string; // The sentence to analyze tree: TreeNode; } interface DecisionTreeWidgetProps { scenarios: TreeScenario[]; accentColor?: string; } type Answers = Record; /** Walk the tree following answers, return ordered list of [node, answer|null] pairs traversed */ function getPath( root: TreeNode, answers: Answers, ): Array<{ node: TreeNode; answer: "yes" | "no" | null }> { const path: Array<{ node: TreeNode; answer: "yes" | "no" | null }> = []; let current: TreeNode | undefined = root; while (current) { // @ts-ignore const ans = answers[current.id] ?? null; path.push({ node: current, answer: ans }); if (ans === null) break; // not answered yet — this is the active node if (current.result !== undefined) break; // leaf current = ans === "yes" ? current.yes : current.no; } return path; } const RESULT_STYLES = { correct: { bg: "bg-green-50", border: "border-green-300", text: "text-green-800", icon: , }, warning: { bg: "bg-amber-50", border: "border-amber-300", text: "text-amber-800", icon: , }, info: { bg: "bg-blue-50", border: "border-blue-300", text: "text-blue-800", icon: , }, }; export default function DecisionTreeWidget({ scenarios, accentColor = "purple", }: DecisionTreeWidgetProps) { const [activeScenario, setActiveScenario] = useState(0); const [answers, setAnswers] = useState({}); const scenario = scenarios[activeScenario]; const path = getPath(scenario.tree, answers); const lastStep = path[path.length - 1]; // reached leaf, no more choices needed // Actually leaf nodes don't have yes/no — they just show result when we arrive const atLeaf = lastStep.node.result !== undefined; const handleAnswer = (nodeId: string, ans: "yes" | "no") => { setAnswers((prev) => { // Remove all answers for nodes that come AFTER this one in the current path const pathIds = path.map((p) => p.node.id); const idx = pathIds.indexOf(nodeId); const newAnswers: Answers = {}; for (let i = 0; i < idx; i++) { newAnswers[pathIds[i]] = prev[pathIds[i]]!; } newAnswers[nodeId] = ans; return newAnswers; }); }; const resetScenario = () => setAnswers({}); const switchScenario = (i: number) => { setActiveScenario(i); setAnswers({}); }; return (
{/* Scenario tab strip */} {scenarios.length > 1 && (
{scenarios.map((sc, i) => ( ))}
)} {/* Sentence under analysis */}

Analyze this sentence

"{scenario.sentence}"

{/* Breadcrumb path */} {path.length > 1 && (
{path.map((step, i) => { if (i === path.length - 1) return null; // last step shown below, not in crumb const isAnswered = step.answer !== null; return ( ); })}
)} {/* Active node */}
{atLeaf ? /* Leaf result */ (() => { const node = lastStep.node; const rType = node.resultType ?? "correct"; const s = RESULT_STYLES[rType]; return (
{s.icon}

{node.result}

{node.ruleRef && (

{node.ruleRef}

)}
); })() : /* Decision question */ (() => { const node = lastStep.node; return (

{node.question}

{node.hint && (

{node.hint}

)} {!node.hint &&
}
); })()}
{/* Footer */}
{atLeaf && scenarios.length > 1 && activeScenario < scenarios.length - 1 && ( )}
); }