{title}
{Array.from({length:total}).map((_,k)=>(
))}
{loading&&
}
{err&&
Couldn't generate — {err}
}
{ex&&!loading&&ex.type==="thesis"&&(
)}
{ex&&!loading&&ex.type!=="thesis"&&(
{ex.prompt}
{(ex.options||[]).map((o,k)=>{
let cls="opt";
if(submitted){ if(k===ex.correct) cls+=" right"; else if(k===sel) cls+=" wrong"; }
else if(k===sel) cls+=" sel";
return (
);
})}
{!submitted&&
}
{submitted&&(
{sel===ex.correct?"Correct.":"Not quite."} {ex.explanation}
)}
)}
{submitted&&
}
{submitted&&(
)}
);
};
// ── LEARN CARD ──────────────────────────────────────────────────────────────
const LearnCard = ({ state, subject, topic, onDone }) => {
const [content,setContent]=useState(null);
const [err,setErr]=useState(null);
useEffect(()=>{ (async()=>{
try{
const r=await callTask("learn",{ subjectName:subject.name, topicName:topic.name,
topicDescription:topic.description, modality:state.modality?.dominant,
subjectContext:P.subjectContext(subject) });
setContent(r);
}catch(e){ setErr(e.message); }
})(); },[]);
return (
{!content&&!err&&
}
{err&&
{err}
}
{content&&(
{content.title}
{content.subtitle}
{(content.points||[]).map((pt,k)=>(
))}
Key takeaway: {content.key_takeaway}
{content.resources?.length>0&&(
)}
)}
);
};
// ── SESSION COMPLETE ────────────────────────────────────────────────────────
const SessionComplete = ({ correct, total, note, onClose }) => (
=0.6?"var(--brand)":"var(--gold)"}/>
Session complete
Your profile has been updated.
{note&&
{note}
}
);
// ── NEW SUBJECT FLOW — skeleton first, then section 1 only ─────────────────
const NewSubjectFlow = ({ state, setState, onClose, onCreated }) => {
const [msgs,setMsgs]=useState([{role:"assistant",content:"What do you want to study? Tell me the subject, your current level, any deadline, and whether you want multiple-choice, written responses, or both."}]);
const [input,setInput]=useState("");
const [loading,setLoading]=useState(false);
const [skeleton,setSkeleton]=useState(null);
const [building,setBuilding]=useState(false);
const bottom=useRef(null);
useEffect(()=>{bottom.current?.scrollIntoView({behavior:"smooth"});},[msgs,skeleton]);
const send=async()=>{
const text=input.trim(); if(!text||loading) return;
setInput(""); const nm=[...msgs,{role:"user",content:text}];
setMsgs(nm); setLoading(true);
try{
const {text:resp}=await callTask("skeleton_chat",{messages:nm.map(m=>({role:m.role,content:m.content}))});
const sk=parseTag(resp,"skeleton");
setMsgs([...nm,{role:"assistant",content:stripTags(resp)||"Here's a proposed outline."}]);
if(sk){ try{ setSkeleton(JSON.parse(sk)); }catch{} }
}catch(e){ setMsgs([...nm,{role:"assistant",content:"Error: "+e.message}]); }
setLoading(false);
};
const confirm=async()=>{
if(!skeleton||building) return;
setBuilding(true);
try{
const {topics}=await callTask("build_section",{skeleton,sectionIndex:0});
const color=SUBJECT_COLORS[state.subjects.length%SUBJECT_COLORS.length];
const sub=await DB.createSubject(skeleton,color,topics||[]);
DB.event("curriculum_created",{subject_id:sub.id});
const np={...state,subjects:[...state.subjects,sub]};
setState(np);
onCreated(sub.id);
}catch(e){ setMsgs(m=>[...m,{role:"assistant",content:"Couldn't create the subject: "+e.message}]); }
setBuilding(false);
};
return (
{msgs.map((m,k)=>
)}
{loading&&
}
{skeleton&&(
Proposed outline — {skeleton.name}
{skeleton.description}
{(skeleton.sections||[]).map((s,k)=>(
))}
Only section 1 is built in detail now — later sections build as you progress, shaped by your performance. Want changes? Just say so in the chat.
)}
setInput(e.target.value)} onKeyDown={e=>e.key==="Enter"&&send()}/>
);
};
// ── CURRICULUM LIST (Google Classroom style) ────────────────────────────────
const CurriculumList = ({ subject, onAction }) => {
const [openTopic,setOpenTopic]=useState(null);
const [collapsed,setCollapsed]=useState({});
const skeleton=subject.curriculum?.skeleton||{};
const sections=skeleton.sections||[];
const topics=P.orderedTopics(subject);
const builtSections=[...new Set(topics.map(t=>t.section_index))];
const meta=(t)=>{
const s=P.topicStatus(subject,t.id);
const m=subject.tp[t.id]?.mastery||0;
const sk=subject.tp[t.id]?.skipped;
if(s==="complete") return {ring:"var(--green)",label:sk?"Tested out":"Mastered",color:"var(--green)",m:sk?100:m};
if(s==="in-progress") return {ring:"var(--violet)",label:`${m}%`,color:"var(--violet)",m};
if(s==="available") return {ring:"var(--line2)",label:"Ready",color:"var(--ink2)",m:0};
return {ring:"var(--line)",label:"Locked",color:"var(--ink3)",m:0};
};
const numberOf={}; topics.forEach((t,k)=>{ numberOf[t.id]=k+1; });
return (
{sections.map((sec,si)=>{
const secTopics=topics.filter(t=>t.section_index===si);
const isBuilt=builtSections.includes(si);
const isCol=collapsed[si];
const done=secTopics.filter(t=>(subject.tp[t.id]?.mastery||0)>=70||subject.tp[t.id]?.skipped).length;
return (
{!isCol&&(isBuilt?(
{secTopics.map(t=>{
const n=numberOf[t.id];
const md=meta(t);
const status=P.topicStatus(subject,t.id);
const locked=status==="locked";
const isOpen=openTopic===t.id;
return (
{isOpen&&!locked&&(
{t.description}
)}
);
})}
):(
{sec.summary} Builds automatically when you clear the section before it.
))}
);
})}
);
};
// ── CURRICULUM EDIT CHAT — surgical edits, automatic versioning ─────────────
const CurriculumEditChat = ({ state, setState, subject, onClose }) => {
const [msgs,setMsgs]=useState([{role:"assistant",content:`I can edit "${subject.name}". Try: "add a topic on X", "remove the second topic", "make section 2 harder", or "reorder so Y comes first." Each change is saved as a new version, so nothing is lost.`}]);
const [input,setInput]=useState("");
const [loading,setLoading]=useState(false);
const [pending,setPending]=useState(null);
const bottom=useRef(null);
useEffect(()=>{bottom.current?.scrollIntoView({behavior:"smooth"});},[msgs,pending]);
const send=async()=>{
const text=input.trim(); if(!text||loading) return;
setInput(""); const nm=[...msgs,{role:"user",content:text}];
setMsgs(nm); setLoading(true); setPending(null);
try{
const curriculum={ sections:subject.curriculum.skeleton?.sections||[],
topics:P.orderedTopics(subject).map(t=>({id:t.id,name:t.name,description:t.description,priority:t.priority,type:t.type,section_index:t.section_index})) };
const {text:resp}=await callTask("edit_chat",{ curriculum, messages:nm.map(m=>({role:m.role,content:m.content})), subjectContext:P.subjectContext(subject) });
const cm=parseTag(resp,"curriculum"), ch=parseTag(resp,"change");
setMsgs([...nm,{role:"assistant",content:stripTags(resp)||"Here's the edit."}]);
if(cm){ try{ const c=JSON.parse(cm); if(c.topics?.length) setPending({curriculum:c,changeType:(ch||"edit").trim()}); }catch{} }
}catch(e){ setMsgs([...nm,{role:"assistant",content:"Error: "+e.message}]); }
setLoading(false);
};
const apply=async()=>{
if(!pending) return;
try{
const newCurr=await DB.applyEdit(subject,pending.curriculum,pending.changeType);
const np={...state,subjects:state.subjects.map(s=>s.id===subject.id?{...s,curriculum:newCurr}:s)};
setState(np);
setMsgs(m=>[...m,{role:"assistant",content:`Saved as version ${newCurr.versionNumber}. Keep editing or close this panel.`}]);
setPending(null);
}catch(e){ setMsgs(m=>[...m,{role:"assistant",content:"Couldn't save: "+e.message}]); }
};
return (
Edit curriculum
{subject.name} · v{subject.curriculum?.versionNumber||1}
{msgs.map((m,k)=>
)}
{loading&&
}
{pending&&(
Proposed · {pending.changeType}
{pending.curriculum.topics.map((t,k)=>(
{k+1}
{t.name}{t.id==="new"&&new}
))}
)}
setInput(e.target.value)} onKeyDown={e=>e.key==="Enter"&&send()}/>
);
};
// ── SUBJECT VIEW ────────────────────────────────────────────────────────────
const SubjectView = ({ state, setState }) => {
const subjects=state.subjects.filter(s=>s.status==="active");
const [creating,setCreating]=useState(false);
const [activeSid,setActiveSid]=useState(subjects[0]?.id||null);
const [mode,setMode]=useState(null); // {kind:'learn'|'practice'|'testout', topic}
const [result,setResult]=useState(null); // {correct,total,note}
const [editing,setEditing]=useState(false);
const [preview,setPreview]=useState(null); // {title,text|loading}
const [toast,setToast]=useState(null);
const sub=subjects.find(s=>s.id===activeSid)||subjects[0]||null;
const showToast=(m)=>{ setToast(m); setTimeout(()=>setToast(null),3500); };
// Progressive section building: when the current section is cleared (75%+
// on every topic), build the next one with awareness of performance so far.
const maybeBuildNext=async(afterSub)=>{
const fresh=afterSub||sub; if(!fresh?.curriculum) return null;
const st=P.sectionStatus(fresh);
if(!(st.cleared&&st.hasNext)) return null;
const already=fresh.curriculum.topics.some(t=>t.section_index===st.nextIndex);
if(already) return null;
try{
const performance=P.orderedTopics(fresh).map(t=>`${t.name}: ${fresh.tp[t.id]?.mastery||0}%`).join(", ");
const {topics}=await callTask("build_section",{ skeleton:{...fresh.curriculum.skeleton,name:fresh.name},
sectionIndex:st.nextIndex, performance, subjectContext:P.subjectContext(fresh) });
const rows=await DB.insertTopics(fresh.curriculum.id,st.nextIndex,topics||[]);
setState(prev=>({...prev,subjects:prev.subjects.map(s=>s.id===fresh.id
?{...s,curriculum:{...s.curriculum,topics:[...s.curriculum.topics,...rows]}}:s)}));
return `Section ${st.nextIndex+1} — "${fresh.curriculum.skeleton.sections[st.nextIndex].title}" — has been built based on your performance.`;
}catch{ return null; }
};
const previewNext=async()=>{
const st=P.sectionStatus(sub);
if(!st.hasNext) return;
const sec=sub.curriculum.skeleton.sections[st.nextIndex];
setPreview({title:sec.title,text:null});
try{
const {text}=await callTask("preview_section",{ sectionTitle:sec.title, sectionSummary:sec.summary,
subjectName:sub.name, subjectContext:P.subjectContext(sub) });
setPreview({title:sec.title,text});
}catch(e){ setPreview({title:sec.title,text:"Couldn't generate a preview: "+e.message}); }
};
const onSessionDone=async(correct,total,exited)=>{
if(!exited){
const np={...state,sessions:state.sessions+1};
setState(np); DB.savePortfolio(np);
DB.event("session_completed",{subject_id:sub?.id,exercises:total,correct});
}
const wasTestOut=mode?.kind==="testout";
let note=null;
if(wasTestOut&&!exited){
if(correct>=2){
const np={...state};
const s2=np.subjects.find(s=>s.id===sub.id);
const tp={...(s2.tp[mode.topic.id]||{mastery:0,correct:0,total:0}),mastery:85,skipped:true};
s2.tp={...s2.tp,[mode.topic.id]:tp};
setState(np); DB.upsertProgress(mode.topic.id,tp);
note=`You tested out of "${mode.topic.name}".`;
} else note=`Not quite — you need 2 of 3 to test out. The topic stays in your path.`;
}
setMode(null);
if(!exited){
const built=await maybeBuildNext();
setResult({correct,total,note:[note,built].filter(Boolean).join(" ")||null});
if(built) showToast("New section unlocked");
}
};
if(creating) return
setCreating(false)} onCreated={(id)=>{setCreating(false);setActiveSid(id);}}/>
;
if(result) return
setResult(null)}/>;
if(mode?.kind==="learn") return setMode(null)}/>;
if(mode?.kind==="practice"||mode?.kind==="testout")
return ;
const st=sub?P.sectionStatus(sub):null;
return (
Subjects
Custom curricula that adapt as you learn.
{subjects.length===0&&(
Nothing here yet
Describe what you want to learn and Cognitif builds the curriculum.
)}
{subjects.length>0&&(
{subjects.map(s=>(
))}
{sub&&(
Curriculum · v{sub.curriculum?.versionNumber||1}
{st?.hasNext&&
}
{sub.curriculum
?
{ if(kind!=="learn") DB.event("session_started",{subject_id:sub.id,mode:kind}); setMode({kind,topic}); }}/>
: No curriculum found for this subject.
}
About
{sub.curriculum?.skeleton?.description||"—"}
{sub.level&&{sub.level}}
{sub.timeConstraint&&{sub.timeConstraint}}
{(sub.exerciseFormats||[]).map(f=>{f})}
Progress
{P.orderedTopics(sub).map(t=>(
=70||sub.tp[t.id]?.skipped?"var(--green)":"var(--violet)"}/>
{t.name}
{sub.tp[t.id]?.skipped?"—":`${sub.tp[t.id]?.mastery||0}%`}
))}
)}
)}
{sub&&!editing&&!mode&&
}
{sub&&editing&&
setEditing(false)}/>}
{preview&&(
setPreview(null)}/>
Up next · {preview.title}
{preview.text===null?
:
{preview.text}
}
)}
);
};
// ── BRAIN TRAINING ──────────────────────────────────────────────────────────
const BrainView = ({ state, setState }) => {
const [training,setTraining]=useState(null); // domain object
const [result,setResult]=useState(null);
if(training) return {
if(!exited){
const np={...state,sessions:state.sessions+1};
setState(np); DB.savePortfolio(np);
DB.event("session_completed",{brain_domain:training.id,exercises:total,correct});
setResult({correct,total});
}
setTraining(null);
}}/>;
if(result) return setResult(null)}/>;
const weakest=[...DOMAINS].sort((a,b)=>(state.brain[a.id]?.score||50)-(state.brain[b.id]?.score||50))[0];
return (
Brain training
Six domains. Sessions adapt to your scores — your weakest right now is {weakest.label}.
{DOMAINS.map(d=>{
const dom=state.brain[d.id]||P.defDomain();
return (
{d.label}
{d.blurb}
{dom.traj==="improving"?"↗ improving":dom.traj==="declining"?"↘ declining":dom.traj==="stable"?"→ stable":"· forming"} · {dom.sessions||0} exercises
);
})}
);
};
// ── DASHBOARD ───────────────────────────────────────────────────────────────
const Dashboard = ({ state, go }) => {
const acc=P.recentAcc(state);
const subjects=state.subjects.filter(s=>s.status==="active");
return (
{state.name?`Welcome back, ${state.name.split(" ")[0]}`:"Welcome back"}
Here's where your mind stands today.
Brain score
Average across six domains
Recent accuracy
{acc!==null?"% over last 12 answers":"No exercises yet"}
Sessions
{state.totalEx} exercises total
Active subjects
{subjects.length===0&&
None yet — create one to get a personalized curriculum.
}
{subjects.map(s=>{
const ord=P.orderedTopics(s);
const done=ord.filter(t=>(s.tp[t.id]?.mastery||0)>=70||s.tp[t.id]?.skipped).length;
return (
go("subjects")}>
{s.name}
{done}/{ord.length} topics
);
})}
Domain snapshot
{DOMAINS.map(d=>{
const dom=state.brain[d.id]||P.defDomain();
return (
{d.label}
);
})}
);
};
// ── PORTFOLIO ───────────────────────────────────────────────────────────────
const PortfolioView = ({ state }) => {
const sub=state.subjects.filter(s=>s.status==="active")[0]||null;
const profileLines=useMemo(()=>{
const lines=["USER PROFILE","Brain Scores (0-100, trajectory):"];
DOMAINS.forEach(d=>{ const dom=state.brain[d.id]||P.defDomain(); lines.push(` ${d.label}: ${Math.round(dom.score)} (${dom.traj})`); });
lines.push(`Learning Modality: ${(state.modality?.dominant||"logical_sequential").replace(/_/g," ")} (confidence ${(state.modality?.confidence??0).toFixed(1)})`);
const acc=P.recentAcc(state);
lines.push(`Recent Accuracy: ${acc!==null?acc+"%":"no data yet"}`);
lines.push(`Sessions Completed: ${state.sessions} · Total Exercises: ${state.totalEx}`);
const qt=state.questionTypePerf||{};
if(Object.keys(qt).length) lines.push(`Performance by Question Type: ${Object.keys(qt).map(k=>`${k} ${qt[k].total?Math.round(qt[k].correct/qt[k].total*100):0}%`).join(", ")}`);
if(state.feedbackSignal?.length) lines.push(`Recent Difficulty Feedback: ${state.feedbackSignal.slice(-8).join(", ")}`);
if(sub) lines.push(P.subjectContext(sub));
return lines.join("\n");
},[state]);
return (
Portfolio
Your full cognitive profile — and exactly what the AI sees when it personalizes for you.
Brain domains
{DOMAINS.map(d=>{
const dom=state.brain[d.id]||P.defDomain();
return (
{d.label}
{dom.traj}
{Math.round(dom.score)}
);
})}
Structured profile — what the AI sees
{profileLines}
Account
Signed in as {state.name||"—"}. All data lives in your Supabase project.
);
};
// ── APP SHELL ───────────────────────────────────────────────────────────────
const App = ({ initial }) => {
const [state,setState]=useState(initial);
const [page,setPage]=useState("dashboard");
const NAV=[["dashboard","Dashboard"],["brain","Brain training"],["subjects","Subjects"],["portfolio","Portfolio"]];
return (
{page==="dashboard"&&}
{page==="brain"&&}
{page==="subjects"&&}
{page==="portfolio"&&}
);
};
// ── ROOT — auth gate ────────────────────────────────────────────────────────
const Root = () => {
const [phase,setPhase]=useState("loading"); // loading | login | name | app
const [initial,setInitial]=useState(null);
const [errMsg,setErrMsg]=useState(null);
const boot=async(user)=>{
try{
const st=await DB.loadAll(user);
setInitial(st);
setPhase(st.name?"app":"name");
}catch(e){ setErrMsg(e.message); setPhase("error"); }
};
useEffect(()=>{
if(SUPABASE_URL.startsWith("YOUR_")){ setPhase("config"); return; }
sb.auth.getSession().then(({data:{session}})=>{ if(session?.user) boot(session.user); else setPhase("login"); });
const {data:{subscription}}=sb.auth.onAuthStateChange((event,session)=>{
if(event==="SIGNED_IN"&&session?.user) boot(session.user);
if(event==="SIGNED_OUT") setPhase("login");
});
return ()=>subscription.unsubscribe();
},[]);
if(phase==="config") return (
Almost there
Open this file and paste your SUPABASE_URL and SUPABASE_ANON_KEY into the CONFIG block at the top of the script. Full steps are in SETUP.md.
);
if(phase==="loading") return
;
if(phase==="error") return
;
if(phase==="login") return
;
if(phase==="name") return
{ setInitial(s=>({...s,name})); setPhase("app"); }}/>;
return ;
};
ReactDOM.createRoot(document.getElementById("root")).render();