From e0d0d73dc3129801bdf6e44a889f519428f3ef84 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Mon, 11 Aug 2025 09:23:59 +0000 Subject: [PATCH] Refactor Rules of Inference Playground Update the Rules of Inference Playground to feature a single active drop zone in the center arena. The layout is changed to have premises and rules in separate columns above the arena. When a premise and rule are dropped, the system will now generate the resulting statements, removing the need for users to drag intermediate statements. The functionality to select a subset of premises for applying an inference rule has also been added. Intermediate statements below the arena have been removed. --- src/components/RulesOfInferencePlayground.tsx | 328 +++++++++++------- 1 file changed, 194 insertions(+), 134 deletions(-) diff --git a/src/components/RulesOfInferencePlayground.tsx b/src/components/RulesOfInferencePlayground.tsx index c6e0ba0..78c0f39 100644 --- a/src/components/RulesOfInferencePlayground.tsx +++ b/src/components/RulesOfInferencePlayground.tsx @@ -122,93 +122,122 @@ export default function RulesOfInferencePlayground() { const [exampleId, setExampleId] = useState(EXAMPLES[0].id); const example = useMemo(() => EXAMPLES.find((e) => e.id === exampleId)!, [exampleId]); - const [lines, setLines] = useState(Array.from({ length: 6 }, () => ({ status: "pending" }))); + type Step = { used: string[]; rule: string; result: string }; + + const [steps, setSteps] = useState([]); + const [activePremises, setActivePremises] = useState([]); + const [activeRule, setActiveRule] = useState(undefined); useEffect(() => { // Reset when example changes - setLines(Array.from({ length: 6 }, () => ({ status: "pending" }))); + setSteps([]); + setActivePremises([]); + setActiveRule(undefined); }, [exampleId]); - const previousStatements = (upto: number) => - lines.slice(0, upto).map((l) => l.statement).filter(Boolean) as string[]; + const availableStatements = useMemo( + () => Array.from(new Set([...(example?.premises ?? []), ...steps.map((s) => s.result)])), + [example, steps] + ); - 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 reachedTarget = steps.some((s) => s.result === example.conclusion); - const setLineRule = (i: number, rule: string) => { - setLines((prev) => { - const next = [...prev]; - next[i] = { ...next[i], rule, status: validateLine(i, { ...next[i], rule }) }; - return next; - }); - }; + // Derivation logic for candidates based on the currently selected subset and rule + function deriveCandidates(rule?: string, selected: string[] = []): string[] { + if (!rule || selected.length === 0) return []; - 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"; + case "distribution": { + // From (p ∧ q) ∨ r derive p ∨ r and q ∨ r + if (selected.includes("(p ∧ q) ∨ r") && selected.length === 1) { + return ["p ∨ r", "q ∨ r"]; } break; - case "implication-elim": - if (stmt === "¬r ∨ s" && prev.includes("r → s")) return "correct"; + } + case "implication-elim": { + // From r → s derive ¬r ∨ s + if (selected.includes("r → s") && selected.length === 1) { + return ["¬r ∨ s"]; + } break; - case "resolution": - if ( - stmt === "p ∨ s" && - prev.includes("p ∨ r") && - prev.includes("¬r ∨ s") - ) - return "correct"; + } + case "resolution": { + // From (p ∨ r) and (¬r ∨ s) derive (p ∨ s) + const hasPorR = selected.includes("p ∨ r"); + const hasNotRorS = selected.includes("¬r ∨ s"); + if (hasPorR && hasNotRorS && selected.length === 2) { + return ["p ∨ s"]; // Focused on intended path for this example + } break; + } } } - // Not validated - return "incorrect"; - }; + return []; + } - const handleStatementDrop = (lineIdx: number, e: React.DragEvent) => { + const candidates = useMemo( + () => deriveCandidates(activeRule, activePremises), + [activeRule, activePremises] + ); + + // Auto-apply when there is exactly one valid candidate + useEffect(() => { + if (candidates.length === 1 && activeRule && activePremises.length > 0) { + applyCandidate(candidates[0]); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [candidates.length]); + + function applyCandidate(result: string) { + if (!activeRule || activePremises.length === 0) return; + + // Validate again defensively + const valid = deriveCandidates(activeRule, activePremises).includes(result); + if (!valid) { + toast({ + title: "Not quite", + description: "That rule doesn’t apply to the selected premises.", + variant: "destructive", + }); + return; + } + + setSteps((prev) => [...prev, { used: [...activePremises], rule: activeRule, result }]); + setActivePremises([]); + setActiveRule(undefined); + + if (result === example.conclusion) { + toast({ title: "Nice!", description: "You derived the target conclusion." }); + } + } + + function handlePremiseDrop(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 doesn’t follow from your chosen rule and prior lines.", variant: "destructive" }); + const value = data.value; + // Only allow from available set + if (!availableStatements.includes(value)) return; + setActivePremises((prev) => (prev.includes(value) ? prev : [...prev, value])); } - }; + } - const handleRuleDrop = (lineIdx: number, e: React.DragEvent) => { + function handleRuleDrop(e: React.DragEvent) { const data = parseDragData(e); if (!data) return; - if (data.type === "rule") { - setLineRule(lineIdx, data.value); - } + if (data.type === "rule") setActiveRule(data.value); + } + + const reset = () => { + setSteps([]); + setActivePremises([]); + setActiveRule(undefined); }; - const reset = () => setLines(Array.from({ length: 6 }, () => ({ status: "pending" }))); - - const reachedTarget = lines.some((l) => l.statement === example.conclusion && l.status === "correct"); + const removeActivePremise = (p: string) => + setActivePremises((prev) => prev.filter((x) => x !== p)); return (
@@ -236,94 +265,125 @@ export default function RulesOfInferencePlayground() {
- {/* Arena */} -
- {/* Left: Premises */} -
-

Premises

+ {/* Above the arena: two columns (Available statements | Rules) */} +
+
+

Available statements

- {example.premises.map((p) => ( - onDragStart(e, "premise", p)} - /> - ))} +
+

Initial premises

+
+ {example.premises.map((p) => ( + onDragStart(e, "premise", p)} /> + ))} +
+
+ {steps.length > 0 && ( +
+

Derived so far

+
+ {steps.map((s, idx) => ( + onDragStart(e, "statement", s.result)} + /> + ))} +
+
+ )}
- {/* Center: Notepad lines with rule margin */} -
-
-
- {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 ( -
- {/* statement area (left) */} - handleStatementDrop(i, e)} - className={"bg-background min-h-12 flex items-center " + ringCls} - > -
- {line.statement ? ( - {line.statement} - ) : ( - Drop a statement or premise here - )} -
-
- - {/* rule margin (right) */} - handleRuleDrop(i, e)}> -
- {line.rule ? {line.rule} : Drop rule} -
-
-
- ); - })} -
- - {/* Target below last line */} -
- Target conclusion: {example.conclusion} -
-
-
- - {/* Right: Rules */}
-

Rules

+

Inference rules

{example.rules.map((r) => (
- onDragStart(e, "rule", r.id)} - /> - {r.help && ( -

{r.help}

- )} + onDragStart(e, "rule", r.id)} /> + {r.help &&

{r.help}

}
))}
- {/* Pool */} -
-

Intermediate statements

-
- {example.pool.map((s) => ( - onDragStart(e, "statement", s)} /> - ))} + {/* Arena (full width) */} +
+
+ {/* History */} + {steps.length > 0 && ( +
+ {steps.map((s, i) => ( +
+
+ {s.result} +
+
+ by {s.rule} + from {s.used.join(", ")} +
+
+ ))} +
+ )} + + {/* Active line */} +
+
+ {/* premises selection zone */} + +
+ {activePremises.length === 0 ? ( + Drop premises here (you can drop multiple) + ) : ( + activePremises.map((p) => ( +
+ {p} + +
+ )) + )} +
+
+ + {/* rule zone */} + +
+ {activeRule ? {activeRule} : Drop rule} +
+
+
+ + {/* Candidate results (if multiple) */} + {activeRule && activePremises.length > 0 && candidates.length > 1 && ( +
+

Possible conclusions:

+
+ {candidates.map((c) => ( + + ))} +
+
+ )} + + {/* Target below */} +
+ Target conclusion: {example.conclusion} +
+