import React, { useEffect, useRef, useState } from “react”;
// Single-file React + Tailwind prototype // Default export a previewable App component for a StudyBuddy landing dashboard
export default function App() { // sample data const [goal] = useState({ title: “Master Intro to Bayesian Reasoning”, subtitle: “4 months · 3–5 hrs/week”, progress: 0.32, nextAction: “Watch: Bayes Intuition (12m)” });
const initialNodes = [ { id: “bayes”, label: “Bayes’ Theorem”, group: “core”, x: 300, y: 100 }, { id: “prob”, label: “Probability Basics”, group: “prereq”, x: 100, y: 300 }, { id: “cond”, label: “Conditional Prob.”, group: “prereq”, x: 500, y: 300 }, { id: “examples”, label: “Worked Examples”, group: “practice”, x: 300, y: 500 }, { id: “common”, label: “Common Fallacies”, group: “meta”, x: 600, y: 100 } ];
const initialLinks = [ { source: “prob”, target: “bayes” }, { source: “cond”, target: “bayes” }, { source: “bayes”, target: “examples” }, { source: “bayes”, target: “common” } ];
const [nodes, setNodes] = useState(initialNodes); const [links] = useState(initialLinks); const [selected, setSelected] = useState(nodes[0]);
// tiny force simulation (very rudimentary) to make graph feel alive const simRef = useRef({}); useEffect(() ⇒ { let raf; const width = 900; const height = 520; const nodeMap = Object.fromEntries(nodes.map((n) ⇒ [n.id, n]));
function step() {
// simple repulsion and link spring
for (let a of nodes) {
a.vx = (a.vx || 0) * 0.9;
a.vy = (a.vy || 0) * 0.9;
// repulse from center softly
const cx = width / 2;
const cy = height / 2;
a.vx += (cx - a.x) * 0.0005;
a.vy += (cy - a.y) * 0.0005;
}
for (let l of links) {
const s = nodeMap[l.source];
const t = nodeMap[l.target];
if (!s || !t) continue;
const dx = t.x - s.x;
const dy = t.y - s.y;
const d = Math.sqrt(dx * dx + dy * dy) || 1;
const desired = 160; // spring length
const k = 0.002; // spring stiffness
const force = (d - desired) * k;
const fx = (dx / d) * force;
const fy = (dy / d) * force;
s.vx += fx;
s.vy += fy;
t.vx -= fx;
t.vy -= fy;
}
// update positions
for (let n of nodes) {
// slight random jitter
n.vx += (Math.random() - 0.5) * 0.02;
n.vy += (Math.random() - 0.5) * 0.02;
n.x += n.vx;
n.y += n.vy;
}
setNodes((prev) => prev.map((p) => ({ ...p }))); // trigger re-render with mutated positions
raf = requestAnimationFrame(step);
}
raf = requestAnimationFrame(step);
simRef.current.raf = raf;
return () => cancelAnimationFrame(raf);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// dragging const dragRef = useRef({ dragging: null, ox: 0, oy: 0 }); function onMouseDownNode(e, node) { e.stopPropagation(); dragRef.current.dragging = node.id; dragRef.current.ox = e.clientX - node.x; dragRef.current.oy = e.clientY - node.y; } useEffect(() ⇒ { function onMouseMove(e) { const id = dragRef.current.dragging; if (!id) return; setNodes((prev) ⇒ prev.map((n) ⇒ (n.id === id ? { …n, x: e.clientX - dragRef.current.ox, y: e.clientY - dragRef.current.oy, vx: 0, vy: 0 } : n)) ); } function onMouseUp() { dragRef.current.dragging = null; } window.addEventListener(“mousemove”, onMouseMove); window.addEventListener(“mouseup”, onMouseUp); return () ⇒ { window.removeEventListener(“mousemove”, onMouseMove); window.removeEventListener(“mouseup”, onMouseUp); }; }, []);
// mock resources per node const resources = { bayes: [ { id: “r1”, title: “Bayes Intuition — 12m video”, kind: “video”, length: “12:03”, excerpt: “Understanding conditional update of beliefs.” }, { id: “r2”, title: “Proof & Examples (Article)”, kind: “article”, length: “6 pages”, excerpt: “Step-by-step derivation and worked problems.” } ], prob: [{ id: “r3”, title: “Probability Primer”, kind: “article”, length: “8 pages”, excerpt: “Basics: events, independence, distributions.” }], cond: [{ id: “r4”, title: “Conditionals in Practice”, kind: “video”, length: “9:21”, excerpt: “Real-world conditional problems.” }], examples: [{ id: “r5”, title: “Practice Set: 15 problems”, kind: “exercise”, length: “15 items”, excerpt: “Mixed difficulty problems” }], common: [{ id: “r6”, title: “Avoiding Common Fallacies”, kind: “article”, length: “4 pages”, excerpt: “Base rate neglect, prosecutor’s fallacy.” }] };
// study pane actions const [activeResource, setActiveResource] = useState(null); const [flashcards, setFlashcards] = useState([]);
function addFlashcard(node, snippet) { setFlashcards((f) ⇒ [{ id: Date.now(), node: node.id, front: snippet, back: “Explain this in your own words.”, due: new Date().toISOString() }, …f]); }
function simulateQuiz() { // very simple: bump mastery alert(“Quiz complete — +4% mastery (simulated)”); }
return (
StudyBuddy — Prototype
Purpose-driven study, argument graphs, and micro-practice
{/* Main grid */}
<div className="grid grid-cols-12 gap-6">
{/* Left: dashboard / goals */}
<aside className="col-span-3">
<div className="bg-white rounded-2xl p-4 shadow-sm">
<h3 className="font-semibold">Goal</h3>
<div className="mt-3">
<div className="text-lg font-bold">{goal.title}</div>
<div className="text-sm text-slate-500">{goal.subtitle}</div>
<div className="w-full bg-slate-200 h-3 rounded-full mt-3 overflow-hidden">
<div style={{ width: `${goal.progress * 100}%` }} className="h-3 bg-gradient-to-r from-emerald-400 to-indigo-600" />
</div>
<div className="mt-2 text-xs text-slate-600">Next: {goal.nextAction}</div>
<button onClick={() => alert('Start next action (simulated)')} className="mt-3 w-full px-3 py-2 bg-indigo-50 text-indigo-700 rounded-md text-sm">Start</button>
</div>
</div>
<div className="bg-white rounded-2xl p-4 shadow-sm mt-4">
<h4 className="font-semibold">Mastery Snapshot</h4>
<div className="mt-3 space-y-2 text-sm">
<div className="flex justify-between"><span>Reasoning</span><span>32%</span></div>
<div className="flex justify-between"><span>Memory</span><span>58%</span></div>
<div className="flex justify-between"><span>Problem Solving</span><span>21%</span></div>
</div>
</div>
<div className="bg-white rounded-2xl p-4 shadow-sm mt-4">
<h4 className="font-semibold">Flashcards</h4>
<div className="mt-3 space-y-2 text-sm max-h-40 overflow-auto">
{flashcards.length === 0 ? (
<div className="text-slate-500">No flashcards yet — create one from a resource.</div>
) : (
flashcards.map((f) => (
<div key={f.id} className="p-2 bg-slate-50 rounded-md">
<div className="text-xs font-medium">{f.node}</div>
<div className="text-sm">{f.front}</div>
</div>
))
)}
</div>
</div>
</aside>
{/* Center: Graph Explorer */}
<main className="col-span-6">
<div className="bg-white rounded-2xl p-4 shadow-sm">
<div className="flex items-center justify-between mb-3">
<h3 className="font-semibold">Graph Explorer</h3>
<div className="text-sm text-slate-500">Click a node to open Study Pane • Drag to rearrange</div>
</div>
<div className="w-full rounded-md border border-slate-100 overflow-hidden">
<svg viewBox="0 0 900 520" width="100%" height={520} className="bg-gradient-to-b from-white to-slate-50">
{/* links */}
{links.map((l, i) => {
const s = nodes.find((n) => n.id === l.source) || { x: 0, y: 0 };
const t = nodes.find((n) => n.id === l.target) || { x: 0, y: 0 };
return <line key={i} x1={s.x} y1={s.y} x2={t.x} y2={t.y} strokeWidth={2} stroke="rgba(99,102,241,0.12)" />;
})}
{/* nodes */}
{nodes.map((n) => (
<g key={n.id} transform={`translate(${n.x},${n.y})`} style={{ cursor: 'pointer' }}>
<circle
r={36}
fill={n.id === selected.id ? 'url(#g)' : n.group === 'core' ? '#6366f1' : '#06b6d4'}
stroke={n.id === selected.id ? '#1f2937' : 'rgba(0,0,0,0.06)'}
strokeWidth={n.id === selected.id ? 2 : 1}
onMouseDown={(e) => onMouseDownNode(e, n)}
onClick={() => setSelected(n)}
/>
<text x={0} y={52} fontSize={12} textAnchor="middle" fill="#111827">
{n.label}
</text>
</g>
))}
<defs>
<linearGradient id="g" x1="0" x2="1">
<stop offset="0%" stopColor="#7c3aed" />
<stop offset="100%" stopColor="#06b6d4" />
</linearGradient>
</defs>
</svg>
</div>
<div className="mt-3 flex gap-2 text-sm">
<button onClick={() => { setNodes((prev) => prev.concat({ id: 'new' + Date.now(), label: 'New Node', group: 'custom', x: 200 + Math.random() * 400, y: 200 + Math.random() * 200 })); }} className="px-3 py-1 rounded-md bg-indigo-50 text-indigo-700">+ Add Node</button>
<button onClick={() => { setNodes(initialNodes); }} className="px-3 py-1 rounded-md bg-slate-50">Reset Layout</button>
<div className="ml-auto text-slate-500">Nodes: {nodes.length}</div>
</div>
</div>
{/* quick inspector */}
<div className="mt-4 bg-white rounded-2xl p-4 shadow-sm">
<div className="flex items-start gap-4">
<div className="w-14 h-14 rounded-md bg-indigo-100 flex items-center justify-center text-indigo-700 font-semibold">{selected.label.split(' ').slice(0,2).map(s=>s[0]).join('')}</div>
<div>
<div className="text-sm font-semibold">{selected.label}</div>
<div className="text-xs text-slate-500">Type: {selected.group}</div>
<div className="mt-2 text-sm text-slate-600">Explore resources, create flashcards, or start a short quiz.</div>
</div>
<div className="ml-auto flex gap-2">
<button onClick={() => setActiveResource(resources[selected.id]?.[0] ?? null)} className="px-3 py-1 bg-emerald-50 text-emerald-700 rounded-md text-sm">Open Resource</button>
<button onClick={() => addFlashcard(selected, `${selected.label}: core idea`)} className="px-3 py-1 bg-slate-50 rounded-md text-sm">+Flashcard</button>
</div>
</div>
</div>
</main>
{/* Right: Study Pane */}
<aside className="col-span-3">
<div className="bg-white rounded-2xl p-4 shadow-sm">
<div className="flex items-center justify-between mb-3">
<h4 className="font-semibold">Study Pane</h4>
<div className="text-xs text-slate-400">Context • Resources • Practice</div>
</div>
{activeResource ? (
<div>
<div className="text-sm font-medium">{activeResource.title}</div>
<div className="text-xs text-slate-500">{activeResource.kind} • {activeResource.length}</div>
<div className="mt-3 text-sm text-slate-700">{activeResource.excerpt}</div>
<div className="mt-4 flex gap-2">
<button onClick={() => { simulateQuiz(); }} className="px-3 py-2 bg-indigo-600 text-white rounded-md">Take Quick Quiz</button>
<button onClick={() => addFlashcard(selected, `${activeResource.title} — key idea`)} className="px-3 py-2 bg-slate-50 rounded-md">Create Flashcard</button>
</div>
<div className="mt-4">
<div className="text-xs text-slate-500">Excerpt highlights</div>
<div className="mt-2 text-sm bg-slate-50 p-3 rounded-md">{activeResource.excerpt} <button onClick={() => addFlashcard(selected, activeResource.excerpt)} className="ml-2 text-emerald-600 text-xs">Save as flashcard</button></div>
</div>
</div>
) : (
<div className="text-sm text-slate-500">Select a node then open a resource. You can also watch suggested videos or start practice sets.</div>
)}
</div>
<div className="bg-white rounded-2xl p-4 shadow-sm mt-4">
<h5 className="font-semibold">Practice</h5>
<div className="mt-3 text-sm text-slate-600">Spaced practice: next review in 3 days</div>
<div className="mt-4 flex gap-2">
<button onClick={() => alert('Start practice (simulated)')} className="px-3 py-2 bg-emerald-50 text-emerald-700 rounded-md">Practice Now</button>
<button onClick={() => alert('View schedule (simulated)')} className="px-3 py-2 bg-slate-50 rounded-md">Schedule</button>
</div>
</div>
<div className="bg-white rounded-2xl p-4 shadow-sm mt-4">
<h5 className="font-semibold">Definitions Timeline</h5>
<div className="mt-3 text-xs text-slate-500">Versioned definitions & provenance — demo only</div>
<div className="mt-3 text-sm">
<div className="text-sm font-medium">{selected.label}</div>
<div className="text-xs text-slate-400">v1 — original formulation (1970)</div>
<div className="text-xs text-slate-400">v2 — modern reframing (2015)</div>
</div>
</div>
</aside>
</div>
</div>
</div>
); }