Add inference rules playground

Create a new interactive playground for practicing rules of inference under Discrete Math > Foundations. The playground will feature a three-column layout for premises, a workspace with parallel lines for derivations, and a column for relevant inference rules. Users can drag and drop elements, apply rules, and receive validation for their steps. The initial example will be based on the provided screenshot.
This commit is contained in:
gpt-engineer-app[bot] 2025-08-11 08:57:30 +00:00
parent 2b22dc0d95
commit 3d2d2fdbff
2 changed files with 339 additions and 0 deletions

View file

@ -0,0 +1,331 @@
import { useEffect, useMemo, useState } from "react";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useToast } from "@/components/ui/use-toast";
// Rules of Inference Playground (Example 1: Resolution)
// Layout: 3 columns (Premises | Notepad lines with rule margin | Rules)
// Below: pool of intermediate statements + distractors
// Interaction: drag premises/clauses onto lines, drag a rule to the small right margin for that line,
// then drag a conclusion from the pool to a new line. We validate the step.
// Types
interface Line {
statement?: string;
rule?: string; // rule id placed in the margin for this line
status?: "pending" | "correct" | "incorrect";
}
type DragType = "premise" | "statement" | "rule";
interface RuleDef {
id: string;
label: string;
help?: string;
}
interface ExampleDef {
id: string;
title: string;
premises: string[];
conclusion: string;
rules: RuleDef[];
pool: string[]; // candidate intermediate and target statements
}
const EXAMPLES: ExampleDef[] = [
{
id: "ex-resolution-1",
title: "(p ∧ q) r, r → s ⊢ p s",
premises: ["(p ∧ q) r", "r → s"],
conclusion: "p s",
rules: [
{
id: "distribution",
label: "Distribution: (A ∧ B) C ≡ (A C) ∧ (B C)",
help: "From (p ∧ q) r you may derive p r or q r.",
},
{
id: "implication-elim",
label: "→ elimination: (A → B) ≡ (¬A B)",
help: "From r → s you may derive ¬r s.",
},
{
id: "resolution",
label: "Resolution",
help: "From (X r) and (¬r Y) derive (X Y).",
},
],
pool: [
"p r",
"q r",
"¬r s",
"p s", // target
// distractors
"p",
"q",
"r",
"s",
"p q",
"¬p s",
],
},
];
// Small helper to attach drag payload
function onDragStart(e: React.DragEvent, type: DragType, value: string) {
e.dataTransfer.setData("text/plain", JSON.stringify({ type, value }));
e.dataTransfer.effectAllowed = "move";
}
function parseDragData(e: React.DragEvent): { type: DragType; value: string } | null {
try {
const raw = e.dataTransfer.getData("text/plain");
return JSON.parse(raw);
} catch {
return null;
}
}
function DraggableChip({ label, onDragStart: handle }: { label: string; onDragStart: (e: React.DragEvent) => void }) {
return (
<div
role="button"
draggable
onDragStart={handle}
className="select-none cursor-grab active:cursor-grabbing rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground shadow-sm hover:bg-accent hover:text-accent-foreground transition-colors"
aria-label={label}
title={label}
>
{label}
</div>
);
}
function DropZone({ children, onDrop, className }: { children?: React.ReactNode; onDrop: (e: React.DragEvent) => void; className?: string }) {
return (
<div
onDragOver={(e) => e.preventDefault()}
onDrop={onDrop}
className={
"min-h-12 rounded-md border border-dashed border-border bg-background/60 p-2 " +
(className ?? "")
}
>
{children}
</div>
);
}
export default function RulesOfInferencePlayground() {
const { toast } = useToast();
const [exampleId, setExampleId] = useState(EXAMPLES[0].id);
const example = useMemo(() => EXAMPLES.find((e) => e.id === exampleId)!, [exampleId]);
const [lines, setLines] = useState<Line[]>(Array.from({ length: 6 }, () => ({ status: "pending" })));
useEffect(() => {
// Reset when example changes
setLines(Array.from({ length: 6 }, () => ({ status: "pending" })));
}, [exampleId]);
const previousStatements = (upto: number) =>
lines.slice(0, upto).map((l) => l.statement).filter(Boolean) as string[];
const setLineStatement = (i: number, statement: string) => {
setLines((prev) => {
const next = [...prev];
next[i] = { ...next[i], statement, status: validateLine(i, { ...next[i], statement }) };
return next;
});
};
const setLineRule = (i: number, rule: string) => {
setLines((prev) => {
const next = [...prev];
next[i] = { ...next[i], rule, status: validateLine(i, { ...next[i], rule }) };
return next;
});
};
const validateLine = (i: number, candidate?: Line): Line["status"] => {
const l = candidate ?? lines[i];
const stmt = l.statement?.trim();
const rule = l.rule;
// Premises allowed with no rule.
if (stmt && example.premises.includes(stmt) && !rule) return "correct";
if (!stmt || !rule) return "pending";
// Gather earlier statements
const prev = previousStatements(i);
// Validation rules specific to this example
if (example.id === "ex-resolution-1") {
switch (rule) {
case "distribution":
if (stmt === "p r" || stmt === "q r") {
if (prev.includes("(p ∧ q) r")) return "correct";
}
break;
case "implication-elim":
if (stmt === "¬r s" && prev.includes("r → s")) return "correct";
break;
case "resolution":
if (
stmt === "p s" &&
prev.includes("p r") &&
prev.includes("¬r s")
)
return "correct";
break;
}
}
// Not validated
return "incorrect";
};
const handleStatementDrop = (lineIdx: number, e: React.DragEvent) => {
const data = parseDragData(e);
if (!data) return;
if (data.type === "premise" || data.type === "statement") {
setLineStatement(lineIdx, data.value);
const status = validateLine(lineIdx, { ...lines[lineIdx], statement: data.value });
if (status === "incorrect")
toast({ title: "Not quite", description: "That statement doesnt follow from your chosen rule and prior lines.", variant: "destructive" });
}
};
const handleRuleDrop = (lineIdx: number, e: React.DragEvent) => {
const data = parseDragData(e);
if (!data) return;
if (data.type === "rule") {
setLineRule(lineIdx, data.value);
}
};
const reset = () => setLines(Array.from({ length: 6 }, () => ({ status: "pending" })));
const reachedTarget = lines.some((l) => l.statement === example.conclusion && l.status === "correct");
return (
<div className="min-h-[70vh] space-y-4">
{/* Top controls */}
<div className="flex items-center gap-3">
<Select value={exampleId} onValueChange={setExampleId}>
<SelectTrigger className="w-[360px]">
<SelectValue placeholder="Choose example" />
</SelectTrigger>
<SelectContent>
{EXAMPLES.map((ex) => (
<SelectItem key={ex.id} value={ex.id}>
{ex.title}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="ml-auto flex items-center gap-2">
{reachedTarget && (
<span className="text-sm text-muted-foreground">Nice! You derived the target.</span>
)}
<Button variant="secondary" onClick={reset}>
Reset
</Button>
</div>
</div>
{/* Arena */}
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
{/* Left: Premises */}
<section aria-label="Premises" className="space-y-2">
<h2 className="text-sm font-medium text-muted-foreground">Premises</h2>
<div className="space-y-2">
{example.premises.map((p) => (
<DraggableChip
key={p}
label={p}
onDragStart={(e) => onDragStart(e, "premise", p)}
/>
))}
</div>
</section>
{/* Center: Notepad lines with rule margin */}
<section aria-label="Workspace" className="relative">
<div className="rounded-lg border border-border bg-card p-2">
<div className="grid grid-rows-6 gap-2">
{Array.from({ length: 6 }).map((_, i) => {
const line = lines[i];
const ringCls =
line.status === "correct"
? "ring-1 ring-primary"
: line.status === "incorrect"
? "ring-1 ring-destructive"
: "";
return (
<div key={i} className="grid grid-cols-[1fr_112px] items-stretch gap-2">
{/* statement area (left) */}
<DropZone
onDrop={(e) => handleStatementDrop(i, e)}
className={"bg-background min-h-12 flex items-center " + ringCls}
>
<div className="px-2 text-sm text-foreground">
{line.statement ? (
<span>{line.statement}</span>
) : (
<span className="text-muted-foreground">Drop a statement or premise here</span>
)}
</div>
</DropZone>
{/* rule margin (right) */}
<DropZone onDrop={(e) => handleRuleDrop(i, e)}>
<div className="flex h-full items-center justify-center rounded-md bg-muted/30 text-xs text-muted-foreground">
{line.rule ? <span>{line.rule}</span> : <span>Drop rule</span>}
</div>
</DropZone>
</div>
);
})}
</div>
{/* Target below last line */}
<div className="mt-3 rounded-md border border-border bg-background p-2 text-sm">
Target conclusion: <span className="font-medium text-foreground">{example.conclusion}</span>
</div>
</div>
</section>
{/* Right: Rules */}
<section aria-label="Rules" className="space-y-2">
<h2 className="text-sm font-medium text-muted-foreground">Rules</h2>
<div className="space-y-2">
{example.rules.map((r) => (
<div key={r.id} className="space-y-1">
<DraggableChip
label={r.id}
onDragStart={(e) => onDragStart(e, "rule", r.id)}
/>
{r.help && (
<p className="text-xs text-muted-foreground pl-1">{r.help}</p>
)}
</div>
))}
</div>
</section>
</div>
{/* Pool */}
<section aria-label="Statements Pool" className="space-y-2">
<h2 className="text-sm font-medium text-muted-foreground">Intermediate statements</h2>
<div className="grid grid-cols-2 gap-2 md:grid-cols-4">
{example.pool.map((s) => (
<DraggableChip key={s} label={s} onDragStart={(e) => onDragStart(e, "statement", s)} />
))}
</div>
</section>
</div>
);
}

View file

@ -9,6 +9,7 @@ import BalancedTernaryGame from "@/components/BalancedTernaryGame";
import ZeckendorfGame from "@/components/ZeckendorfGame";
import ZeckendorfSearchTrick from "@/components/ZeckendorfSearchTrick";
import KnightsAndKnavesI from "@/components/KnightsAndKnavesI";
import RulesOfInferencePlayground from "@/components/RulesOfInferencePlayground";
interface Interactive {
id: string;
title: string;
@ -52,6 +53,13 @@ const interactives: Interactive[] = [{
tags: ["logic", "puzzle", "knights", "knaves", "truth-tellers"],
component: KnightsAndKnavesI,
greenScreenPath: "/themes/discrete-math/foundations"
}, {
id: "rules-of-inference",
title: "Rules of Inference Playground",
description: "Practice applying resolution and equivalence rules to derive conclusions step by step.",
tags: ["logic", "proofs", "inference", "resolution", "deduction"],
component: RulesOfInferencePlayground,
greenScreenPath: "/themes/discrete-math/foundations"
}];
const Foundations = () => {
const [searchTerm, setSearchTerm] = useState("");