chore(build): refactor codebase for production

This commit is contained in:
shafin-r
2026-03-12 02:39:34 +06:00
parent 121cc2bf71
commit bd35f6a852
123 changed files with 3501 additions and 3254 deletions

View File

@ -1,22 +1,28 @@
import React, { useState } from 'react';
import { ChevronRight, RotateCcw, CheckCircle2, AlertTriangle, Info } from 'lucide-react';
import React, { useState } from "react";
import {
ChevronRight,
RotateCcw,
CheckCircle2,
AlertTriangle,
Info,
} from "lucide-react";
export interface TreeNode {
id: string;
question: string;
question?: string;
hint?: string;
yesLabel?: string;
noLabel?: string;
yes?: TreeNode;
no?: TreeNode;
result?: string;
resultType?: 'correct' | 'warning' | 'info';
resultType?: "correct" | "warning" | "info";
ruleRef?: string;
}
export interface TreeScenario {
label: string; // Short tab label, e.g. "Sentence 1"
sentence: string; // The sentence to analyze
label: string; // Short tab label, e.g. "Sentence 1"
sentence: string; // The sentence to analyze
tree: TreeNode;
}
@ -25,59 +31,66 @@ interface DecisionTreeWidgetProps {
accentColor?: string;
}
type Answers = Record<string, 'yes' | 'no'>;
type Answers = Record<string, "yes" | "no">;
/** 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 }> = [];
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;
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',
bg: "bg-green-50",
border: "border-green-300",
text: "text-green-800",
icon: <CheckCircle2 className="w-5 h-5 text-green-600 shrink-0 mt-0.5" />,
},
warning: {
bg: 'bg-amber-50',
border: 'border-amber-300',
text: 'text-amber-800',
bg: "bg-amber-50",
border: "border-amber-300",
text: "text-amber-800",
icon: <AlertTriangle className="w-5 h-5 text-amber-600 shrink-0 mt-0.5" />,
},
info: {
bg: 'bg-blue-50',
border: 'border-blue-300',
text: 'text-blue-800',
bg: "bg-blue-50",
border: "border-blue-300",
text: "text-blue-800",
icon: <Info className="w-5 h-5 text-blue-600 shrink-0 mt-0.5" />,
},
};
export default function DecisionTreeWidget({ scenarios, accentColor = 'purple' }: DecisionTreeWidgetProps) {
export default function DecisionTreeWidget({
scenarios,
accentColor = "purple",
}: DecisionTreeWidgetProps) {
const [activeScenario, setActiveScenario] = useState(0);
const [answers, setAnswers] = useState<Answers>({});
const scenario = scenarios[activeScenario];
const path = getPath(scenario.tree, answers);
const lastStep = path[path.length - 1];
const isLeaf = lastStep.node.result !== undefined;
const isComplete = isLeaf && lastStep.answer === null; // reached leaf, no more choices needed
// 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 => {
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 pathIds = path.map((p) => p.node.id);
const idx = pathIds.indexOf(nodeId);
const newAnswers: Answers = {};
for (let i = 0; i < idx; i++) {
@ -107,7 +120,7 @@ export default function DecisionTreeWidget({ scenarios, accentColor = 'purple' }
className={`px-4 py-2.5 text-sm font-medium whitespace-nowrap transition-colors ${
i === activeScenario
? `bg-white border-b-2 border-${accentColor}-600 text-${accentColor}-700`
: 'text-gray-500 hover:text-gray-700'
: "text-gray-500 hover:text-gray-700"
}`}
>
{sc.label}
@ -117,9 +130,17 @@ export default function DecisionTreeWidget({ scenarios, accentColor = 'purple' }
)}
{/* Sentence under analysis */}
<div className={`px-5 py-4 border-b border-gray-100 bg-${accentColor}-50`}>
<p className={`text-xs font-semibold uppercase tracking-wider text-${accentColor}-500 mb-1`}>Analyze this sentence</p>
<p className="text-gray-800 font-medium italic leading-relaxed">"{scenario.sentence}"</p>
<div
className={`px-5 py-4 border-b border-gray-100 bg-${accentColor}-50`}
>
<p
className={`text-xs font-semibold uppercase tracking-wider text-${accentColor}-500 mb-1`}
>
Analyze this sentence
</p>
<p className="text-gray-800 font-medium italic leading-relaxed">
"{scenario.sentence}"
</p>
</div>
{/* Breadcrumb path */}
@ -133,22 +154,36 @@ export default function DecisionTreeWidget({ scenarios, accentColor = 'purple' }
<button
onClick={() => {
// Reset from this node forward
const pathIds = path.map(p => p.node.id);
const pathIds = path.map((p) => p.node.id);
const idx = pathIds.indexOf(step.node.id);
setAnswers(prev => {
setAnswers((prev) => {
const newAnswers: Answers = {};
for (let j = 0; j < idx; j++) newAnswers[pathIds[j]] = prev[pathIds[j]]!;
for (let j = 0; j < idx; j++)
newAnswers[pathIds[j]] = prev[pathIds[j]]!;
return newAnswers;
});
}}
className={`px-2 py-0.5 rounded transition-colors ${
isAnswered ? 'text-gray-600 hover:text-gray-900 hover:bg-gray-200' : 'text-gray-400'
isAnswered
? "text-gray-600 hover:text-gray-900 hover:bg-gray-200"
: "text-gray-400"
}`}
>
{step.node.question.length > 40 ? step.node.question.slice(0, 40) + '…' : step.node.question}
{
// @ts-ignore
step.node.question.length > 40
? // @ts-ignore
step.node.question.slice(0, 40) + "…"
: step.node.question
}
{step.answer && (
<span className={`ml-1 font-semibold ${step.answer === 'yes' ? 'text-green-600' : 'text-red-500'}`}>
{step.answer === 'yes' ? (step.node.yesLabel ?? 'Yes') : (step.node.noLabel ?? 'No')}
<span
className={`ml-1 font-semibold ${step.answer === "yes" ? "text-green-600" : "text-red-500"}`}
>
{" "}
{step.answer === "yes"
? (step.node.yesLabel ?? "Yes")
: (step.node.noLabel ?? "No")}
</span>
)}
</button>
@ -161,55 +196,61 @@ export default function DecisionTreeWidget({ scenarios, accentColor = 'purple' }
{/* Active node */}
<div className="px-5 py-5">
{atLeaf ? (
/* Leaf result */
(() => {
const node = lastStep.node;
const rType = node.resultType ?? 'correct';
const s = RESULT_STYLES[rType];
return (
<div className={`rounded-xl border p-4 ${s.bg} ${s.border}`}>
<div className="flex gap-3">
{s.icon}
<div>
<p className={`font-semibold ${s.text} leading-snug`}>{node.result}</p>
{node.ruleRef && (
<p className={`mt-2 text-sm font-mono ${s.text} opacity-80 bg-white/60 rounded px-2 py-1 inline-block`}>
{node.ruleRef}
{atLeaf
? /* Leaf result */
(() => {
const node = lastStep.node;
const rType = node.resultType ?? "correct";
const s = RESULT_STYLES[rType];
return (
<div className={`rounded-xl border p-4 ${s.bg} ${s.border}`}>
<div className="flex gap-3">
{s.icon}
<div>
<p className={`font-semibold ${s.text} leading-snug`}>
{node.result}
</p>
)}
{node.ruleRef && (
<p
className={`mt-2 text-sm font-mono ${s.text} opacity-80 bg-white/60 rounded px-2 py-1 inline-block`}
>
{node.ruleRef}
</p>
)}
</div>
</div>
</div>
</div>
);
})()
) : (
/* Decision question */
(() => {
const node = lastStep.node;
return (
<div>
<p className="font-semibold text-gray-800 text-base leading-snug mb-1">{node.question}</p>
{node.hint && <p className="text-sm text-gray-500 mb-4">{node.hint}</p>}
{!node.hint && <div className="mb-4" />}
<div className="flex flex-wrap gap-3">
<button
onClick={() => handleAnswer(node.id, 'yes')}
className="flex-1 min-w-[140px] px-4 py-3 rounded-xl border-2 border-green-300 bg-green-50 text-green-800 font-semibold text-sm hover:bg-green-100 transition-colors"
>
{node.yesLabel ?? 'Yes'}
</button>
<button
onClick={() => handleAnswer(node.id, 'no')}
className="flex-1 min-w-[140px] px-4 py-3 rounded-xl border-2 border-red-200 bg-red-50 text-red-700 font-semibold text-sm hover:bg-red-100 transition-colors"
>
{node.noLabel ?? 'No'}
</button>
);
})()
: /* Decision question */
(() => {
const node = lastStep.node;
return (
<div>
<p className="font-semibold text-gray-800 text-base leading-snug mb-1">
{node.question}
</p>
{node.hint && (
<p className="text-sm text-gray-500 mb-4">{node.hint}</p>
)}
{!node.hint && <div className="mb-4" />}
<div className="flex flex-wrap gap-3">
<button
onClick={() => handleAnswer(node.id, "yes")}
className="flex-1 min-w-35 px-4 py-3 rounded-xl border-2 border-green-300 bg-green-50 text-green-800 font-semibold text-sm hover:bg-green-100 transition-colors"
>
{node.yesLabel ?? "Yes"}
</button>
<button
onClick={() => handleAnswer(node.id, "no")}
className="flex-1 min-w-35 px-4 py-3 rounded-xl border-2 border-red-200 bg-red-50 text-red-700 font-semibold text-sm hover:bg-red-100 transition-colors"
>
{node.noLabel ?? "No"}
</button>
</div>
</div>
</div>
);
})()
)}
);
})()}
</div>
{/* Footer */}
@ -221,14 +262,16 @@ export default function DecisionTreeWidget({ scenarios, accentColor = 'purple' }
<RotateCcw className="w-3.5 h-3.5" />
Try again
</button>
{atLeaf && scenarios.length > 1 && activeScenario < scenarios.length - 1 && (
<button
onClick={() => switchScenario(activeScenario + 1)}
className={`ml-auto flex items-center gap-1.5 text-sm font-semibold text-${accentColor}-700 hover:text-${accentColor}-900 transition-colors`}
>
Next sentence <ChevronRight className="w-4 h-4" />
</button>
)}
{atLeaf &&
scenarios.length > 1 &&
activeScenario < scenarios.length - 1 && (
<button
onClick={() => switchScenario(activeScenario + 1)}
className={`ml-auto flex items-center gap-1.5 text-sm font-semibold text-${accentColor}-700 hover:text-${accentColor}-900 transition-colors`}
>
Next sentence <ChevronRight className="w-4 h-4" />
</button>
)}
</div>
</div>
);